Remote desktop (RustDesk)¶
Self-hosted RustDesk — the open-source TeamViewer/AnyDesk alternative — gives the household screen sharing and remote control without a third-party cloud. The server runs on the VPS, but it is reachable only over the tailnet — nothing listens on the public internet.
How it works¶
flowchart LR
mac["MacBook · tablet<br/>+ family devices"]
wan(("Public<br/>internet"))
subgraph vps["VPS · byers-vps · 100.75.121.110"]
hbbs["hbbs · ID / rendezvous"]
hbbr["hbbr · relay"]
end
mac -. "tailnet · rust.byers.cc" .-> vps
wan -- "21115–21119" --> fw["✕ UFW default-deny"]
classDef blocked opacity:0.55,stroke-dasharray:4 4;
class fw blocked;
Dotted = tailnet overlay · ✕ = dropped at the firewall (no path to the server).
- hbbs — ID / rendezvous server: registers devices and brokers the peer-to-peer handshake.
- hbbr — relay: forwards the already end-to-end-encrypted stream only when a direct link can't be punched. It cannot decrypt anything.
- Both are the one
rustdesk/rustdesk-serverDocker image, run withnetwork_mode: hostunder/opt/rustdesk/. Stood up by/home/elliott/rustdesk-setup/rustdesk-setup.sh(idempotent, run as root).
The deployment¶
| Item | Value |
|---|---|
| Server | VPS, Docker compose at /opt/rustdesk/docker-compose.yml |
| Daemons | hbbs + hbbr, both started with -k _ (force encryption + auto-generate key) |
| Keys | /opt/rustdesk/data/id_ed25519 (private, 0600, never leaves the box) + id_ed25519.pub |
| Public key (client "Key" field) | kqA+g98Teh9K1Dt1lxfKEtvKHhVSZgAaF+8IIF57AOA= — a public key, safe to hand to any client |
| Address | rust.byers.cc → 100.75.121.110 (the VPS's tailnet IP) |
| DNS | Cloudflare A record, DNS-only (grey cloud) — must NOT be proxied |
Why the DNS record is grey-cloud, and why there's no TLS
RustDesk speaks raw TCP/UDP, not HTTP, so Cloudflare's proxy can't carry it — and
pointing the name at a 100.x tailnet address means it resolves publicly but only
connects from tailnet members. There is no Let's Encrypt certificate: the Ed25519
key is the encryption and anti-MITM trust layer. Never put the RustDesk ports
behind NPM.
Ports & firewall¶
| Port | Proto | Role | Public | Tailnet |
|---|---|---|---|---|
| 21115 | tcp | NAT-type test (hbbs) | closed | open |
| 21116 | tcp + udp | registration · hole-punch (hbbs) | closed | open |
| 21117 | tcp | relay (hbbr) | closed | open |
| 21118 / 21119 | tcp | web-client WebSocket (unused) | closed | open |
rustdesk-setup.sh sets UFW to default deny incoming plus allow in on tailscale0, so
every RustDesk port is reachable only across the tailnet.
Public exposure verified closed — 2026-08-12
External scan of 138.68.187.177 from off-tailnet: 21115–21119 all closed; the
same ports are open over the tailnet (100.75.121.110). Because the containers use
network_mode: host (no -p publish), Docker adds no DNAT rules that bypass UFW —
this resolves the RustDesk port-review flagged in the
August audit. SSH 2222 and NPM 80/443 were unaffected.
Reconcile the two UFW scripts (operator)
rustdesk-setup.sh runs its own ufw --force enable on top of vps-harden.sh.
Confirm with sudo ufw status verbose that the merged rule set still contains
WireGuard 51820/udp and the Bedrock UDP ports — i.e. the RustDesk run didn't drop
anything the Minecraft path needs. (Docker-published Minecraft ports bypass UFW anyway,
but WireGuard 51820 does not.)
Security posture¶
- No public attack surface. The classic self-hosted-RustDesk risks — open relay abuse, brute force, DoS — are eliminated because nothing listens publicly. This is the headline control.
- Encryption enforced (
-k _): sessions are always encrypted and clients without the matching public key are rejected. - OSS has no users / 2FA / audit. Access to each machine is guarded by that device's own RustDesk password. The shared key is un-revocable except by regenerating it (which then reconfigures every client) — so never hand it to anyone untrusted.
- Patch the clients. The real standing RustDesk CVEs are client-side; keep the app updated everywhere.
- Back up
/opt/rustdesk/data(key pair + db) so a rebuild doesn't force reconfiguring every client.
Add a new device¶
Every RustDesk device — the one you control from and the one you connect to — must be on the tailnet, because the server has no public door.
- Join the tailnet. Follow Adding a device to the tailnet.
Least-trusted devices (a family member's Mac) should get the
tag:familytag so the ACL can fence them to the RustDesk server only. -
Install RustDesk (rustdesk.com) and point it at the server — Settings → Network → Unlock, then set:
Field Value ID Server rust.byers.ccRelay / API Server (blank) Key kqA+g98Teh9K1Dt1lxfKEtvKHhVSZgAaF+8IIF57AOA=Or configure one device, ≡ → Export Server Config, and Import on the rest. 3. Connect: enter the target's 9-digit RustDesk ID + its access password. A lock icon confirms the session is encrypted.
byers-vps.tail822821.ts.net / 100.75.121.110 also work as the ID Server
If rust.byers.cc won't resolve on a device, the VPS's MagicDNS name or raw tailnet IP
are drop-in substitutes — handy while DNS settles (see the gotcha below).
Unattended access (e.g. a parent's Mac you support)¶
To connect without anyone clicking Accept at the far end:
- RustDesk → Security → set a permanent password, and enable run as service / start on boot.
- Grant the macOS permissions in System Settings → Privacy & Security: Screen Recording, Accessibility, and (macOS 13+) Input Monitoring for RustDesk. Without them you get a black screen / no control. RustDesk's main window links straight to each toggle. Controlling the login screen (before anyone is logged in) needs the service install.
- Easiest done in person; remotely, walk them through RustDesk's own permission prompts, or bootstrap with whatever remote tool already works on their Mac.
Fence family devices with an ACL¶
A new tailnet device otherwise inherits the ops subnet route and can reach the whole LAN. Scope least-trusted devices to the RustDesk server only:
{
"tagOwners": { "tag:family": ["autogroup:admin"] },
"acls": [
{ "action": "accept", "src": ["tag:family"], "dst": ["100.75.121.110:21115-21119"] }
]
}
They can then reach RustDesk (sessions relay through hbbr) but nothing else on the estate. This dovetails with the tailnet ACL tightening already on the operator list.
Gotchas¶
\"Tailscale is stopped\" — check it first
As with everything on the tailnet: if a client can't connect, confirm Tailscale is actually running on both ends before suspecting the server. See Tailscale and Unreachable.
New DNS name → stale negative cache (~30 min)
Right after rust.byers.cc was created, clients kept failing to resolve it because the
UniFi router (192.168.1.1) had cached the NXDOMAIN from before the record existed
(SOA negative TTL 1800s). It self-clears within 30 min; to skip the wait, use the MagicDNS
name / IP above, or set a Tailscale global nameserver of 1.1.1.1 so lookups bypass
the router cache.
Rebuild & maintenance¶
# rebuild from scratch (installs Tailscale, runs containers, sets UFW)
sudo bash ~/rustdesk-setup/rustdesk-setup.sh
# update the server image
cd /opt/rustdesk && sudo docker compose pull && sudo docker compose up -d
# health
sudo docker compose -f /opt/rustdesk/docker-compose.yml logs -f hbbs
tailscale status