Add the RustDesk server playbook
This commit is contained in:
1 parent
fc31504f57
commit
d248675643
7 files changed
+273
No files matched your search
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user