From 035587e3bfa774082efcb17d36bcbb69dcef94cd Mon Sep 17 00:00:00 2001 From: windyboy Date: Wed, 12 Aug 2026 17:35:55 +0800 Subject: [PATCH] feat(rustdesk): onboard self-hosted RustDesk server on hk2 Deploy hbbs + hbbr via a safe-by-default Ansible role (playbooks/rustdesk.yml): with rustdesk_confirm=false it only reports whether compose.yml matches live state and refuses to recreate the stack; with rustdesk_confirm=true it deploys and recreates. The relay assert rejects the known-bad hk2.wsvc.info hostname. Add health profiles rustdesk (hbbs/hbbr health, relay DNS) and hk2aux (co-located traefik/adguard/remark42 on hk2), plus the rustdesk-health runbook and AGENTS.md entry. Server image pinned rustdesk/rustdesk-server:1.1.14. --- AGENTS.md | 7 ++ ansible/playbooks/rustdesk.yml | 20 +++++ .../roles/healthcheck/templates/hk2aux.sh.j2 | 56 ++++++++++++ .../healthcheck/templates/rustdesk.sh.j2 | 25 ++++++ ansible/roles/rustdesk/defaults/main.yml | 13 +++ ansible/roles/rustdesk/tasks/main.yml | 85 +++++++++++++++++++ .../roles/rustdesk/templates/compose.yml.j2 | 34 ++++++++ runbooks/rustdesk-health.md | 80 +++++++++++++++++ 8 files changed, 320 insertions(+) create mode 100644 ansible/playbooks/rustdesk.yml create mode 100644 ansible/roles/healthcheck/templates/hk2aux.sh.j2 create mode 100644 ansible/roles/healthcheck/templates/rustdesk.sh.j2 create mode 100644 ansible/roles/rustdesk/defaults/main.yml create mode 100644 ansible/roles/rustdesk/tasks/main.yml create mode 100644 ansible/roles/rustdesk/templates/compose.yml.j2 create mode 100644 runbooks/rustdesk-health.md diff --git a/AGENTS.md b/AGENTS.md index c3f6e4d..fb878f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,8 @@ Also readable as `agent.md` (symlink → this file). **Matrix (ESS on synapse):** Matrix homeserver running on `synapse.chans.xyz` via the official ESS (Element Server Suite) Helm chart with Synapse + MAS + Element Web + Admin. DNS zone `chans.xyz` managed by hk2 PowerDNS. Before changing config, read [docs/matrix-upstream.md](docs/matrix-upstream.md) and [hosts/synapse.chans.xyz.md](hosts/synapse.chans.xyz.md). K3s cluster on this node has hostPort 80/443 for Traefik (no ServiceLB). Health: [matrix-health](runbooks/matrix-health.md). +**RustDesk:** Self-hosted RustDesk server on `hk2.chans.xyz` (`/opt/rustdesk`, containers `hbbs`/`hbbr`, image pinned `1.1.14`). The `hbbs -r` relay hostname must resolve to the host's public IP `154.36.174.161` — use `hk2.chans.xyz` (never `hk2.wsvc.info`, which has no DNS record). Health: [rustdesk-health](runbooks/rustdesk-health.md). + ## Runbooks & scripts | Task | Path | @@ -71,6 +73,7 @@ Also readable as `agent.md` (symlink → this file). | PowerDNS health (hk2) | [runbooks/pdns-health.md](runbooks/pdns-health.md) | | PowerDNS upstream refs | [docs/pdns-upstream.md](docs/pdns-upstream.md) | | Matrix health | [runbooks/matrix-health.md](runbooks/matrix-health.md) | +| RustDesk health (hk2) | [runbooks/rustdesk-health.md](runbooks/rustdesk-health.md) | | AdGuard Home health | [runbooks/adguard-home-health.md](runbooks/adguard-home-health.md) | | Matrix upstream refs | [docs/matrix-upstream.md](docs/matrix-upstream.md) | | Hermes Agent Matrix channel | [docs/hermes-matrix.md](docs/hermes-matrix.md) | @@ -90,6 +93,10 @@ Issues are tracked in Linear and created/updated via the Linear MCP (`vps` proje Default triage labels: needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See `docs/agents/triage-labels.md`. +### Domain docs + +Single-context layout: `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. + ## Safety - Never commit secrets: passwords, API keys, private keys, `.env`, `mailcow.conf` DB passwords, Vaultwarden `ADMIN_TOKEN` / `.smtp-credentials`. diff --git a/ansible/playbooks/rustdesk.yml b/ansible/playbooks/rustdesk.yml new file mode 100644 index 0000000..939d04c --- /dev/null +++ b/ansible/playbooks/rustdesk.yml @@ -0,0 +1,20 @@ +--- +# Deploy/reconcile the self-hosted RustDesk server (hbbs + hbbr) on hk2. +# +# Safe by default: run with --check for a read-only report, or supply +# rustdesk_confirm=true to deploy the compose file and recreate the stack. +# +# # Read-only report +# ansible-playbook playbooks/rustdesk.yml --limit rustdesk --check +# +# # Apply (deploy compose + recreate hbbs/hbbr) +# ansible-playbook playbooks/rustdesk.yml --limit rustdesk \ +# -e '{"rustdesk_confirm": true}' +- name: Deploy and reconcile RustDesk server + hosts: rustdesk + become: true + gather_facts: false + serial: 1 + roles: + - role: rustdesk + tags: [rustdesk, mutating] diff --git a/ansible/roles/healthcheck/templates/hk2aux.sh.j2 b/ansible/roles/healthcheck/templates/hk2aux.sh.j2 new file mode 100644 index 0000000..4c78d43 --- /dev/null +++ b/ansible/roles/healthcheck/templates/hk2aux.sh.j2 @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -uo pipefail +source '{{ healthcheck_install_root }}/health-common.sh' + +require_command docker +require_command ss + +# Auxiliary services co-located on hk2.chans.xyz (separate compose projects +# under /opt, fronted by Traefik). Verified live 2026-08-12. + +# traefik +if docker inspect traefik >/dev/null 2>&1; then + [[ "$(docker inspect traefik --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" == true ]] \ + && record ok 'traefik_running' || record critical 'traefik_not_running' +else + record critical 'traefik_container_missing' +fi + +# adguardhome (hk2 variant: DoH 5443, DoT 853) +if docker inspect adguardhome >/dev/null 2>&1; then + [[ "$(docker inspect adguardhome --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" == true ]] \ + && record ok 'adguard_running' || record critical 'adguard_not_running' +else + record critical 'adguard_container_missing' +fi + +# remark42 +if docker inspect remark42 >/dev/null 2>&1; then + [[ "$(docker inspect remark42 --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" == true ]] \ + && record ok 'remark42_running' || record critical 'remark42_not_running' +else + record critical 'remark42_container_missing' +fi + +# nginx-manager was removed 2026-08-12 (leftover config, never running). +# Warn if a container by that name ever reappears. +if docker inspect nginx-manager >/dev/null 2>&1; then + record warning 'nginx_manager_unexpectedly_running' +else + record ok 'nginx_manager_not_running' +fi + +# Listening ports (Traefik 80/443/8080, AdGuard DoH 5443 / DoT 853). +ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)80$' \ + && record ok 'traefik_http_80' || record critical 'traefik_http_80_missing' +ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)443$' \ + && record ok 'traefik_https_443' || record critical 'traefik_https_443_missing' +ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)8080$' \ + && record ok 'traefik_dashboard_8080' || record warning 'traefik_dashboard_8080_missing' +ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)5443$' \ + && record ok 'adguard_doh_5443' || record critical 'adguard_doh_5443_missing' +ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)853$' \ + && record ok 'adguard_dot_853' || record critical 'adguard_dot_853_missing' + +emit_result +exit "$EXIT_CODE" diff --git a/ansible/roles/healthcheck/templates/rustdesk.sh.j2 b/ansible/roles/healthcheck/templates/rustdesk.sh.j2 new file mode 100644 index 0000000..f07a2ac --- /dev/null +++ b/ansible/roles/healthcheck/templates/rustdesk.sh.j2 @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -uo pipefail +source '{{ healthcheck_install_root }}/health-common.sh' + +require_command docker +require_command dig + +# hbbs / hbbr must both be running (separate compose project at /opt/rustdesk). +output="$(docker compose --project-directory /opt/rustdesk ps --all 2>&1)" +if grep -qiE 'Exited|Restarting|[[:space:]]Dead[[:space:]]' <<<"$output"; then + record critical 'rustdesk_unhealthy_container' +else + record ok 'rustdesk_compose_ok' +fi + +# hbbs must advertise the relay hostname that resolves to this host's public IP. +cmd="$(docker inspect hbbs --format '{{ '{{' }}json .Config.Cmd{{ '}}' }}' 2>/dev/null)" || record critical 'rustdesk_hbbs_missing' +grep -q 'hk2.chans.xyz:21117' <<<"$cmd" || record critical 'rustdesk_relay_misconfigured' + +# The advertised relay hostname must resolve to this host's public IP. +resolved="$(dig +short hk2.chans.xyz A 2>/dev/null)" +grep -q '154.36.174.161' <<<"$resolved" || record critical 'rustdesk_relay_dns_missing' + +emit_result +exit "$EXIT_CODE" diff --git a/ansible/roles/rustdesk/defaults/main.yml b/ansible/roles/rustdesk/defaults/main.yml new file mode 100644 index 0000000..f7893f0 --- /dev/null +++ b/ansible/roles/rustdesk/defaults/main.yml @@ -0,0 +1,13 @@ +--- +# RustDesk server deployment (hbbs + hbbr) on hk2. +# Safe by default: without rustdesk_confirm=true the role only reports whether +# the declared compose file matches live state and refuses to recreate the stack. +rustdesk_confirm: false +# Compose project directory. +rustdesk_compose_dir: /opt/rustdesk +# Relay (hbbr) hostname:port advertised to every client via `hbbs -r`. +# MUST resolve to this host's public IP (154.36.174.161). The known-bad value +# 'hk2.wsvc.info' has no DNS record and must never be used. +rustdesk_relay: hk2.chans.xyz:21117 +# Pinned server image (used for both hbbs and hbbr). +rustdesk_image: rustdesk/rustdesk-server:1.1.14 diff --git a/ansible/roles/rustdesk/tasks/main.yml b/ansible/roles/rustdesk/tasks/main.yml new file mode 100644 index 0000000..530c072 --- /dev/null +++ b/ansible/roles/rustdesk/tasks/main.yml @@ -0,0 +1,85 @@ +--- +# Deploy/reconcile the self-hosted RustDesk server (hbbs + hbbr). +# Idempotent: deploys the declared compose file; only recreates the stack with +# explicit confirmation. + +- name: Validate relay address is set and not the known-bad value + ansible.builtin.assert: + that: + - rustdesk_relay | length > 0 + - "'hk2.wsvc.info' not in rustdesk_relay" + fail_msg: >- + rustdesk_relay must be a resolvable relay address. The known-bad + 'hk2.wsvc.info' has no DNS record and must not be used. + +- name: Ensure compose project directory exists + ansible.builtin.file: + path: "{{ rustdesk_compose_dir }}" + state: directory + owner: windy + group: root + mode: "0755" + +- name: Deploy compose file + ansible.builtin.template: + src: compose.yml.j2 + dest: "{{ rustdesk_compose_dir }}/compose.yml" + owner: windy + group: windy + mode: "0644" + register: rustdesk_compose_deployed + +- name: Report no change needed + ansible.builtin.debug: + msg: "compose.yml already matches declared state; no change needed." + when: not rustdesk_compose_deployed.changed + +- name: Refuse to recreate without explicit confirmation + ansible.builtin.fail: + msg: >- + compose.yml differs from declared state but rustdesk_confirm is not true. + Supply rustdesk_confirm=true to deploy the file and recreate the stack. + when: + - rustdesk_compose_deployed.changed + - not (rustdesk_confirm | bool) + - not ansible_check_mode + +- name: Apply compose stack + ansible.builtin.command: + argv: + - docker + - compose + - --project-directory + - "{{ rustdesk_compose_dir }}" + - up + - -d + when: + - rustdesk_compose_deployed.changed + - rustdesk_confirm | bool + changed_when: true + register: rustdesk_apply + +- name: Verify hbbs relay command + ansible.builtin.command: + argv: + - docker + - inspect + - hbbs + - --format + - '{{ "{{" }}json .Config.Cmd{{ "}}" }}' + register: rustdesk_hbbs_cmd + changed_when: false + when: + - rustdesk_compose_deployed.changed + - rustdesk_confirm | bool + - not ansible_check_mode + +- name: Assert hbbs advertises the declared relay + ansible.builtin.assert: + that: + - "'{{ rustdesk_relay }}' in rustdesk_hbbs_cmd.stdout" + fail_msg: "hbbs is not advertising the declared relay {{ rustdesk_relay }}." + when: + - rustdesk_compose_deployed.changed + - rustdesk_confirm | bool + - not ansible_check_mode diff --git a/ansible/roles/rustdesk/templates/compose.yml.j2 b/ansible/roles/rustdesk/templates/compose.yml.j2 new file mode 100644 index 0000000..3673a53 --- /dev/null +++ b/ansible/roles/rustdesk/templates/compose.yml.j2 @@ -0,0 +1,34 @@ +networks: + rustdesk-net: + external: false + +services: + hbbs: + container_name: hbbs + ports: + - 21115:21115 + - 21116:21116 + - 21116:21116/udp + - 21118:21118 + image: {{ rustdesk_image }} + command: "hbbs -r {{ rustdesk_relay }}" + volumes: + - ./hbbs:/root + networks: + - rustdesk-net + depends_on: + - hbbr + restart: unless-stopped + + hbbr: + container_name: hbbr + ports: + - 21117:21117 + - 21119:21119 + image: {{ rustdesk_image }} + command: hbbr + volumes: + - ./hbbr:/root + networks: + - rustdesk-net + restart: unless-stopped diff --git a/runbooks/rustdesk-health.md b/runbooks/rustdesk-health.md new file mode 100644 index 0000000..0b90998 --- /dev/null +++ b/runbooks/rustdesk-health.md @@ -0,0 +1,80 @@ +# RustDesk server health (hk2) + +Read-only checks for the `/opt/rustdesk` stack on **hk2.chans.xyz**. + +Facts: [hosts/hk2.chans.xyz.md](../hosts/hk2.chans.xyz.md) + +Prefer the Ansible health report for routine checks and the +[rustdesk playbook](../ansible/playbooks/rustdesk.yml) for reviewed config +changes. Keep the SSH commands below for focused diagnosis. + +## Focused SSH diagnostics + +Use these only after the Ansible health report needs investigation. + +### Quick remote + +```bash +ssh -4 windy@hk2.chans.xyz 'cd /opt/rustdesk && docker compose ps -a' +``` + +Expect `hbbs` and `hbbr` both `Up`. Ignore any stopped orphans unless cleaning up. + +### Version / image pin + +Expect `rustdesk/rustdesk-server:1.1.14` (or the documented pin). Startup logs +should show the relay list and no errors. + +```bash +ssh -4 windy@hk2.chans.xyz 'docker inspect hbbs --format "{{.Config.Image}}"; docker inspect hbbr --format "{{.Config.Image}}"' +``` + +### Relay address (the critical check) + +The `hbbs -r :21117` value is advertised to every client as the relay +(hbbr) address. It **must resolve to this host's public IP** `154.36.174.161`. +`hk2.chans.xyz` is the correct value; `hk2.wsvc.info` has no DNS record and must +never be used. + +```bash +# The value hbbs is actually serving (definitive): +ssh -4 windy@hk2.chans.xyz 'docker inspect hbbs --format "{{json .Config.Cmd}}"' +# expect ["hbbs","-r","hk2.chans.xyz:21117"] + +# Confirm the relay hostname resolves to the public IP: +dig +short hk2.chans.xyz A @1.1.1.1 # expect 154.36.174.161 + +# Confirm the relay hostname is what hbbs logs as its relay list: +ssh -4 windy@hk2.chans.xyz 'docker logs hbbs 2>&1 | grep "relay-servers" | tail -1' +# expect relay-servers=["hk2.chans.xyz:21117"] +``` + +### Ports (from laptop / WSL) + +```bash +IP=154.36.174.161 +nc -vz -w 5 $IP 21117 # hbbr relay TCP +nc -vz -w 5 $IP 21116 # hbbs rendezvous TCP +nc -vz -u -w 5 $IP 21116 # hbbs rendezvous UDP +nc -vz -w 5 $IP 21115 # hbbs NAT type test +``` + +## Scheduled local check + +The sanitized Ansible health profile is `rustdesk` (deployed via +`ansible/playbooks/healthchecks.yml` on hosts whose inventory lists it). It runs +locally through `vps-healthcheck.timer`, writes a sanitized JSON result to +`/var/lib/vps-health/latest.json`, and never exposes secrets. + +## After config changes + +Reviewed changes to `/opt/rustdesk/compose.yml` (relay address, image pin, port +mapping) are applied through the gated rustdesk playbook: + +```bash +cd ansible +ansible-playbook playbooks/rustdesk.yml --limit rustdesk \ + -e '{"rustdesk_confirm": true}' +``` + +Re-run the health report afterwards.