Add the RustDesk server playbook

This commit is contained in:
tanshu committed 2026-10-18 21:35:52 +05:30
1 parent fc31504f57
commit d248675643
7 files changed
+273

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
#################################################
# RustDesk Server Playbook
#################################################
---
- name: Deploy the RustDesk ID/relay server behind Caddy
hosts: monoco
become: true
vars_files:
- vars/default.yml
roles:
- network
- rustdesk
- caddy
+116
View File
@@ -0,0 +1,116 @@
# RustDesk Server on Docker, behind Caddy
Deploys the self-hosted [RustDesk](https://rustdesk.com) server (OSS) on `monoco`: the
ID/rendezvous server `hbbs` and the relay server `hbbr`, both supervised by s6 inside the
official `rustdesk/rustdesk-server-s6` container, reachable only through Caddy on
`rust.tanshu.com:443`. No RustDesk port is published on the host.
## How the traffic is carried
RustDesk's native transports on 21115/21116/21117 are a custom TCP/UDP protocol, not HTTP,
so an HTTP reverse proxy cannot carry them. What *is* HTTP is the WebSocket transport that
`hbbs` and `hbbr` also listen on (21118 and 21119), and clients that enable WebSocket use
it for both rendezvous and relay. Caddy terminates TLS on 443 and forwards two paths into
the container over the Docker network:
| Path | Upstream | Purpose |
| --- | --- | --- |
| `/ws/id` | `rustdesk:21118` (hbbs) | ID registration and signalling |
| `/ws/relay` | `rustdesk:21119` (hbbr) | Relayed sessions |
The code that decides this is in the client: `hbb_common::websocket::check_ws` maps a
domain `host:21116` to `wss://host/ws/id` and `host:21117` to `wss://host/ws/relay`, and
the path names are fixed there. Caddy sets `X-Real-IP`, which `hbbs`/`hbbr` trust to keep
the real client address for rate limiting and logs (upstream warns that these ports must
therefore never be exposed directly).
## Settings
- `host`: the public name, `rust.tanshu.com`. Both the Caddy site and the address `hbbs`
advertises come from it.
- `docker_container`, `image_name`, `image_version`: container name and the
`rustdesk/rustdesk-server-s6` tag to run.
- `data_location`: host directory bind-mounted to `/data` (`/var/lib/rustdesk/data`),
holding the SQLite database and the server key pair.
- `rustdesk_relay_server`: handed to clients by `hbbs` as the relay address. It keeps the
public name, so clients rewrite it to `wss://rust.tanshu.com/ws/relay`.
- `rustdesk_publish_raw_ports` / `rustdesk_raw_ports`: publish 21115/21116/21117 as well.
Off by default, see "Clients that cannot use WebSocket" below.
- `rustdesk_encrypted_only`: `ENCRYPTED_ONLY=1`, so `hbbr` validates the server key too
instead of accepting unkeyed clients.
- `rustdesk_always_use_relay`: `ALWAYS_USE_RELAY=Y`, forcing relayed sessions. WebSocket
clients are relay-only anyway, so this only affects clients on the raw ports.
## Prerequisites
- A DNS `A` record for `rust.tanshu.com` pointing at `monoco`; Caddy issues the certificate
over ports 80/443.
- The Caddy container already running on `monoco` (`caddy/playbook.yml`), with its Caddyfile
at `/var/lib/caddy/conf/Caddyfile`.
## Running this Playbook
The inventory in `/etc/ansible/hosts` already lists `monoco`, which is what `hosts:`
targets:
```command
ansible-playbook playbook.yml
```
The playbook creates the `rustdesk_net` network, attaches Caddy to it, starts the
container, appends the site block to the Caddyfile, reloads Caddy, and prints the public
key.
## Configuring the clients
On each client, open **Settings → Network → ID/Relay Server** and set:
| Field | Value |
| --- | --- |
| ID Server | `rust.tanshu.com` |
| API Server | `https://rust.tanshu.com` |
| Key | the printed value, i.e. `/var/lib/rustdesk/data/id_ed25519.pub` on the host |
then enable **Settings → Network → Use WebSocket** (`allow-websocket=Y`).
The **API Server** field looks odd for an OSS deployment, which has no API, but it is what
selects the TLS scheme: the client uses `wss://` only when the API server starts with
`https`, and otherwise silently falls back to plaintext `ws://rust.tanshu.com/...` on port
80. Because the client also skips its own client↔server key exchange whenever WebSocket is
enabled — on the assumption that the transport is already encrypted — that fallback would
leave the connection unprotected rather than merely failing.
## Trade-offs
- **Relayed sessions only.** With WebSocket enabled the client never hole-punches
(`force_relay` is set, and `hbbs` skips the same-intranet path for WebSocket peers), so
every session flows through `monoco`. Remote control works, but throughput is bounded by
the server, and latency is higher than a direct connection.
- **Upstream labels this a Pro feature.** The client's own tooltip says "RustDesk server
OSS doesn't include this feature", even though the OSS binaries do listen on 21118/21119
and the OSS docs describe proxying them. Treat the WSS path as needing a real test: if
clients fail to register, the fallback is the raw ports below.
- **The address has to be the domain.** `check_ws` only builds a TLS URL and the `/ws`
paths for hostnames; an IP would become `ws://ip:21118` and bypass Caddy entirely.
### Clients that cannot use WebSocket
Clients without the WebSocket option (older builds, third-party clients, or anything that
must connect directly) need the native ports. Set `rustdesk_publish_raw_ports: true` and
re-run the playbook, then open 21115-21116 (TCP) plus 21116/udp and 21117 (TCP) on the host
and in any cloud firewall:
```command
ufw allow 21115:21117/tcp
ufw allow 21116/udp
```
WebSocket clients keep working at the same time; they ignore the raw ports.
## Notes
- **Logs.** `docker logs rustdesk` covers both services.
- **Server key.** If `/var/lib/rustdesk/data/id_ed25519` is lost, every client has to be
reconfigured with the new public key. Back it up.
- **Upgrading.** Re-running the playbook pulls the tag in `image_version` and recreates the
container; the key pair and database live in `data_location` and survive.
+5
View File
@@ -0,0 +1,5 @@
---
# Handlers for Caddy role
- name: Reload Caddy configuration
ansible.builtin.command: "docker exec -w /etc/caddy {{ caddy_container }} caddy reload"
listen: "Reload Caddy"
+22
View File
@@ -0,0 +1,22 @@
---
- name: Read current Caddyfile
ansible.builtin.slurp:
path: "{{ caddyfile_path }}"
register: caddyfile_raw
- name: Decode Caddyfile content
set_fact:
caddyfile_content: "{{ caddyfile_raw['content'] | b64decode }}"
- name: Check if snippet already exists
set_fact:
snippet_present: "{{ snippet_block in caddyfile_content }}"
- name: Add snippet if missing
ansible.builtin.blockinfile:
path: "{{ caddyfile_path }}"
marker: "# {mark} Ansible managed Caddy snippet for {{ host }}"
block: "{{ snippet_block }}"
create: yes
when: not snippet_present
notify: "Reload Caddy"
+10
View File
@@ -0,0 +1,10 @@
---
# Caddy is the only thing that talks to hbbs/hbbr, so it joins the network the
# RustDesk container lives on and can resolve it by container name.
- name: "Ensure {{ docker_network }} Docker network exists"
docker_network:
name: "{{ docker_network }}"
state: present
connected:
- "{{ caddy_container }}"
appends: yes
+50
View File
@@ -0,0 +1,50 @@
---
# Tasks for the RustDesk server role
- name: Pull RustDesk server image
docker_image:
name: "{{ image_name }}:{{ image_version }}"
source: pull
force_source: yes
- name: Ensure the RustDesk data directory exists
file:
path: "{{ data_location }}"
state: directory
mode: 0755
# One container runs both hbbs and hbbr under s6. Its WebSocket ports are left
# unpublished: Caddy reaches them over the shared Docker network, so clients
# only ever connect to 443.
- name: Create the RustDesk server container
docker_container:
name: "{{ docker_container }}"
image: "{{ image_name }}:{{ image_version }}"
state: started
restart_policy: "unless-stopped"
env: "{{ rustdesk_env }}"
published_ports: "{{ rustdesk_published_ports }}"
volumes:
- "{{ data_location }}:/data"
networks:
- name: "{{ docker_network }}"
- name: Wait for hbbs to write out its key pair
ansible.builtin.wait_for:
path: "{{ data_location }}/id_ed25519.pub"
timeout: 60
register: rustdesk_key_file
failed_when: false
- name: Read the public key clients have to be configured with
ansible.builtin.slurp:
src: "{{ data_location }}/id_ed25519.pub"
register: rustdesk_public_key
when: rustdesk_key_file is succeeded
- name: Show the connection details
ansible.builtin.debug:
msg: >-
RustDesk clients: ID Server {{ host }}, API Server https://{{ host }},
enable "Use WebSocket", and use
{{ rustdesk_public_key['content'] | b64decode | trim }} as the key.
when: rustdesk_public_key is not skipped
+56
View File
@@ -0,0 +1,56 @@
---
docker_container: rustdesk
image_name: "rustdesk/rustdesk-server-s6"
image_version: "latest"
# Public name clients use, and the name Caddy serves. hbbs hands this out as the
# relay address, and a WebSocket client rewrites a hostname to
# wss://<host>/ws/relay (an IP would get neither TLS nor the path).
host: rust.tanshu.com
rustdesk_relay_server: "{{ host }}:21117"
host_directory: rustdesk
data_location: "/var/lib/{{ host_directory }}/data"
docker_network: "{{ docker_container }}_net"
caddy_container: caddy
# hbbs/hbbr also speak their own TCP/UDP protocol on 21115-21117, which is not
# HTTP and so cannot go through Caddy. Leave this off to keep the host's only
# open ports at 80/443: clients then have to enable WebSocket, and every session
# is relayed. Turn it on for clients that do not use WebSocket.
rustdesk_publish_raw_ports: false
rustdesk_raw_ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
- "21117:21117"
rustdesk_published_ports: "{{ rustdesk_raw_ports if rustdesk_publish_raw_ports | bool else [] }}"
# hbbs writes id_ed25519/id_ed25519.pub under /data on first start. Clients are
# configured with the public key, and ENCRYPTED_ONLY makes hbbr check it too.
rustdesk_encrypted_only: true
# "Y" pushes every session through the relay. WebSocket clients are relay-only
# regardless, so this only matters for clients on the raw ports.
rustdesk_always_use_relay: false
rustdesk_env:
RELAY: "{{ rustdesk_relay_server }}"
ENCRYPTED_ONLY: "{{ '1' if rustdesk_encrypted_only | bool else '0' }}"
ALWAYS_USE_RELAY: "{{ 'Y' if rustdesk_always_use_relay | bool else 'N' }}"
# Caddy terminates TLS on 443 and carries the WebSocket transports: 21118 is
# hbbs's WebSocket listener, 21119 hbbr's. Neither is published on the host.
caddyfile_path: /var/lib/caddy/conf/Caddyfile
snippet_block: |
{{ host }} {
reverse_proxy /ws/id* {{ docker_container }}:21118 {
header_up X-Real-IP {remote_host}
}
reverse_proxy /ws/relay* {{ docker_container }}:21119 {
header_up X-Real-IP {remote_host}
}
}