Self-hosted RustDesk ID/relay server (hbbs + hbbr)
running on a stock ASUSWRT-Merlin router - no PC, NAS, VPS or Docker needed.
One command installs the server, firewall rules, boot autostart, a daily
self-updater, a crash watchdog, an SSH management TUI and a native page in the
router's own web interface. The router is the natural edge box for this job: it
is already always-on, it owns the WAN connection, and because the server runs
on the router the ports are plain INPUT firewall rules on the router itself -
not port-forwards to yet another machine that has to stay awake behind it.
Verified platform: ASUS RT-BE92U (aarch64, ASUSWRT-Merlin 3006.102.x). Any Merlin router with Entware and a supported CPU should work - see Requirements.
Why self-host instead of TeamViewer or AnyDesk? Both are closed-source, so you can't audit what happens to your session data, and their commercial-use licensing has caught more than one user off guard - by default every connection also routes through their infrastructure. RouterDesk flips that: open-source top to bottom, self-hosted on hardware you already own, no account, no subscription, no third party in the data path.
- One-click installer - single self-contained POSIX
shscript,curl | shsafe (nothing executes until the last line, so a truncated download cannot half-run). Safe to re-run: server key, peer DB and settings are preserved. - Conf-driven services - Entware
init.dservices read/opt/rustdesk/rustdesk.conf; change settings without editing scripts. - Daily self-update at 05:00 with run-test, health check and automatic rollback to the previous binaries if the new ones do not come up.
- Crash watchdog every 5 minutes - restarts dead daemons, respects an
intentional
rustdesk stop, and halts itself after 3 consecutive failed restarts instead of hammering a broken install. - Native router webUI page - shows up in the router's own web interface under the left-menu Addons section (scMerlin-style mount, with icon): status badges, start/stop/restart, update-now, settings form, log viewer, public-key copy button.
- TUI + CLI control script -
rustdeskwith no arguments is an interactive menu over SSH; every action is also a scriptable subcommand. - Key enforcement (
-k _) - only clients presenting your server's public key may register or use the relay, blocking open-relay abuse. - CGNAT detection - the installer checks whether the router really has a public IPv4 and warns honestly when remote IPv4 access cannot work.
- IPv4 + IPv6 firewall automation - idempotent
INPUTrules (not port-forwards), with optionalip6tablesmirroring. - Relay bandwidth tuning knobs - optional hbbr per-connection / total bandwidth caps via the conf file.
- Key regeneration -
rustdesk regen-keymints a new server identity, keeping timestamped backups of the old key. - Clean uninstall - removes everything it installed while preserving the
server identity (key + peer DB) unless you ask for
--purge.
- ASUS router running ASUSWRT-Merlin 3006.x with an aarch64 (arm64v8)
or armv7 CPU -
rustdesk-serverpublishes binaries for these two only. - Entware installed on a USB drive (via
amtm; the drive must stay plugged in - services live under/opt). - JFFS custom scripts enabled: Administration > System > "Enable JFFS custom scripts and configs" = Yes.
- Disk: the server itself needs ~10 MB under
/opt; keep a few tens of MB free for the backup generation and update staging. - RAM: 512 MB or more recommended. Below that the installer warns and
strongly advises adding a swap file of 1 GB+ first (
amtm>sw); routers with 512 MB-1 GB also benefit from swap for headroom. - A DDNS hostname that resolves directly to the router (plain A and
optionally AAAA record -
myrouter.asuscomm.comor your own domain). No Cloudflare proxy / HTTP CDN: RustDesk speaks raw TCP/UDP on the 2111x ports. - The native webUI page additionally needs addon-API firmware support
(
am_addons, present on current Merlin builds); the installer skips it gracefully where unsupported.
SSH into the router as the admin user and run:
curl -fsSL https://raw.githubusercontent.com/medking82/RouterDesk/main/install.sh | shThe installer prompts interactively (it reads from /dev/tty, so prompts work
fine under curl | sh): relay/ID hostname, key enforcement, IPv6 rules, webUI
page. Prefer to read before you pipe? Download install.sh, inspect it, then
sh install.sh - identical result.
For non-interactive runs, pre-seed the answers via environment variables:
| Variable | Meaning | Default |
|---|---|---|
RD_RELAY_HOST |
Relay/ID hostname handed to clients (your DDNS name) | prompted (required) |
RD_ENFORCE_KEY |
Y/N - require the server key to register/relay |
Y |
RD_ENABLE_IPV6 |
Y/N - also open the ports via ip6tables |
Y when IPv6 is active |
RD_WEBUI |
Y/N - install the native webUI page |
Y when firmware supports it |
RD_LOWRAM_OK |
Y - proceed on a <512 MB router without asking |
unset (prompts) |
curl -fsSL https://raw.githubusercontent.com/medking82/RouterDesk/main/install.sh \
| RD_RELAY_HOST=ddns.example.com RD_ENFORCE_KEY=Y RD_ENABLE_IPV6=N RD_WEBUI=Y shOn re-runs, current settings become the prompt defaults; binaries and scripts are refreshed, key/DB/conf are preserved.
install.sh is fully self-contained (every router file ships inside it as an
embedded payload), so only the one script needs to reach the router:
git clone https://github.com/medking82/RouterDesk.git
scp RouterDesk/install.sh admin@192.168.50.1:/tmp/
ssh admin@192.168.50.1
sh /tmp/install.sh(Use your router admin username and LAN IP.) Prefer to see what it automates,
step by step? Read docs/runbook.md - the full manual
walkthrough, useful for porting to non-Merlin routers.
On every RustDesk client (controller and controlled): Settings > Network > unlock > ID/Relay Server:
| Field | Value |
|---|---|
| ID server | ddns.example.com |
| Relay server | ddns.example.com |
| API server | (leave blank - OSS) |
| Key | <your server public key> |
Where to find the key: it is printed at the end of the install, shown by
rustdesk status on the router, available via the webUI page's copy button, or
cat /opt/rustdesk/id_ed25519.pub. After applying, the client's status dot
should go green ("Ready").
rustdesk <command>| Command | What it does |
|---|---|
start / stop / restart |
Manage hbbs (ID server) + hbbr (relay) |
status |
Status report; exit code 0 = both running |
status-web |
Machine-readable KEY=VALUE status (webUI backend; always exit 0) |
logs [hbbs|hbbr|update] |
Show last log lines (default: both services) |
update |
Run the self-updater now |
watchdog |
Cron backend: restart daemons that died |
regen-key [--yes] |
New server identity; old key kept as timestamped .bak files |
set-relay <host> |
Change the relay/ID host, restart hbbs |
set-key-enforce Y|N |
Set key enforcement (-k _), restart services |
set-relay-always Y|N |
Set ALWAYS_USE_RELAY (skip hole-punch), restart hbbs |
set-ipv6 Y|N |
Set ENABLE_IPV6 and apply/remove the live ip6tables rules |
toggle-key |
Flip key enforcement (shorthand) |
toggle-relay-always |
Flip ALWAYS_USE_RELAY (shorthand) |
uninstall [--purge] [--yes] |
Remove the server (--purge also deletes key/DB/conf) |
rustdesk stop writes an intentional-stop marker so the watchdog leaves the
daemons alone; start/restart (and a reboot) clear it.
rustdesk with no arguments opens an interactive menu (status view plus
numbered actions for everything above).
If installed, the router web interface gains a native Addons > RustDesk
page (re-login once if it does not appear immediately): live hbbs/hbbr status
badges, listening ports, installed version, the server public key with a copy
button, start/stop/restart/update buttons, a settings form (relay host, key
enforcement, always-relay, IPv6) and a log viewer. It is a thin shell over the
same rustdesk control script, wired through the stock Merlin addon mechanism
(see docs/gui-addon-design.md).
Everything lives in /opt/rustdesk/rustdesk.conf (sourced by the services;
edit via the rustdesk setters, or by hand + rustdesk restart):
| Key | Meaning | Default |
|---|---|---|
RELAY_HOST |
Hostname handed to clients as the relay (-r <host>:21117) |
set at install |
ENFORCE_KEY |
Y = run hbbs/hbbr with -k _ (key required) |
Y |
ENABLE_IPV6 |
Y = maintain ip6tables INPUT rules |
asked at install |
ALWAYS_USE_RELAY |
Y = tell clients to skip hole-punching, go straight to relay |
N |
RD_ARCH |
Release asset tag used by the updater (arm64v8/armv7) |
detected |
SINGLE_BANDWIDTH |
Optional: hbbr per-connection cap, Mb/s | unset (upstream 128) |
TOTAL_BANDWIDTH |
Optional: hbbr whole-relay cap, Mb/s | unset (upstream 1024) |
LIMIT_SPEED |
Optional: hbbr throttle for downgraded connections, Mb/s | unset (upstream 32) |
DOWNGRADE_THRESHOLD |
Optional: hbbr downgrade trigger ratio | unset (upstream 0.66) |
The four bandwidth knobs are advanced hbbr tuning: leave them unset and hbbr
uses its upstream defaults; set a value and rustdesk restart to override.
A daily cron job (05:00) runs /opt/rustdesk/update.sh; rustdesk update
runs the same thing on demand. It resolves the newest rustdesk-server tag via
the GitHub releases/latest redirect (no API, no rate limit), and does nothing
further if the installed version matches. On a new release it downloads the
right zip for your CPU, run-tests the new binaries (with a patchelf fallback
for odd libcs), backs up the running ones to *.bak, hot-swaps, restarts, then
health-checks that the ports are listening - and rolls back to the *.bak
binaries automatically if they are not. History lands in
/opt/rustdesk/update.log; the installed tag in /opt/rustdesk/VERSION.
Your server key and peer DB are never touched by updates - clients keep
working. (The RustDesk client apps update themselves independently.)
If your ISP puts you behind CGNAT (no public IPv4 on the router's WAN), then remote access over IPv4 is impossible no matter which ports you open - there is no public address for clients to reach. The installer detects this (WAN IP in private/CGNAT ranges, or WAN IP differing from an external echo) and says so instead of letting you debug a config that can never work. Your real options:
- IPv6 end-to-end - if both the router and your remote clients have working IPv6, publish an AAAA record and enable the IPv6 rules (next section). Many CGNAT'd ISPs and most mobile carriers do provide IPv6.
- Ask your ISP for a public IPv4 - often a paid add-on.
- LAN/VPN only - keep the server private and reach it through a VPN into your LAN (e.g. WireGuard), then connect as if local.
Answering yes to IPv6 (or rustdesk set-ipv6 Y) sets ENABLE_IPV6=Y and
maintains ip6tables INPUT rules for the same ports; the firewall block
re-reads the setting on every firewall restart, so it survives reboots and
firmware firewall reloads. hbbs/hbbr already listen dual-stack.
The DNS half is yours: publish an AAAA record for your DDNS name pointing at the router's global IPv6 address (some DDNS providers update AAAA automatically, others need a custom script). Once both halves are in place, clients with working IPv6 - including phones on CGNAT'd mobile carriers - can connect direct over IPv6 even when your IPv4 is unreachable.
What this exposes on the WAN (as router INPUT rules, not forwards):
| Port | Proto | Service | Open on WAN | Purpose |
|---|---|---|---|---|
| 21115 | TCP | hbbs | yes | NAT type test |
| 21116 | TCP + UDP | hbbs | yes | ID registration / heartbeat / hole-punch |
| 21117 | TCP | hbbr | yes | Relay traffic |
| 21118 / 21119 | TCP | hbbs / hbbr | no | Web client (listening, not firewall-opened) |
- Keep key enforcement ON (the default). With
-k _, only clients presenting your server's public key can register or relay - without it, any internet stranger can discover and use your relay for their own traffic. - Keys and DB stay on the router.
/opt/rustdesk/id_ed25519is the private key and never leaves the box;id_ed25519.pubis what you hand to clients;db_v2.sqlite3holds registered peers. Back upid_ed25519*anddb_v2.sqlite3off-router - they are excluded from this repo by design, and losing the key means reconfiguring every client against a new one. - webUI data sits behind the router admin session. The page and its status files are served by the router's httpd inside the authenticated UI; nothing secret enters the web directory (public key, process state, log tails only).
- Relay sessions cap at ~30 fps by upstream RustDesk design (rustdesk/rustdesk discussion #11804). Direct P2P sessions are not capped; this only bites when traffic falls back to the relay.
- Relay traffic rides your home upload. Fine for occasional support sessions; sustained HD streaming through hbbr is limited by your uplink (see the bandwidth knobs above if you need to cap it).
- Phone clients have decode ceilings. Streaming a 4K desktop to a phone can be limited by the phone's H.265/HEVC decode capability - a client-device limit, not a server one.
- The 05:00 updater restarts a manually-stopped server when a new release
lands (an update implies "should run", and it clears the stop marker). This
is documented behavior, not a bug: if you want the server down long-term,
uninstall it or remove the
RustDeskUpdatecron job (cru d RustDeskUpdate).
sh /opt/rustdesk/uninstall.sh # or: rustdesk uninstall
sh /opt/rustdesk/uninstall.sh --purge # also delete key, peer DB and confWithout --purge it removes services, cron jobs, firewall rules, the control
script and the webUI page, but keeps the server identity
(id_ed25519/.pub), the peer DB and your settings - re-running install.sh
later picks them up unchanged and your clients keep working. --purge deletes
those too; every client would need repointing afterwards. Only the marker
blocks this installer added to /jffs/scripts/* are stripped - your own lines
in those files are untouched.
Paths mirror where files land on the router:
install.sh one-click installer; embeds every file below as a payload
uninstall.sh clean removal -> /opt/rustdesk/uninstall.sh
opt/etc/init.d/S98hbbr hbbr init.d service -> /opt/etc/init.d/S98hbbr
opt/etc/init.d/S99hbbs hbbs init.d service -> /opt/etc/init.d/S99hbbs
opt/rustdesk/update.sh daily self-updater -> /opt/rustdesk/update.sh
opt/bin/rustdesk control script (TUI/CLI) -> /opt/bin/rustdesk
webui/rustdesk.asp native webUI page -> /jffs/addons/rustdesk/rustdesk.asp
webui/mount-webui.sh webUI mount/glue -> /jffs/addons/rustdesk/mount-webui.sh
jffs/scripts/* reference copies of the firewall/boot hooks the
installer writes as marker blocks on the live router
docs/runbook.md the full manual walkthrough (what install.sh automates)
docs/gui-addon-design.md webUI addon architecture + integration contract
scripts/ payload regen/verify tooling (see scripts/README.md)
install.sh is generated-in-place: the embedded heredoc payloads are
byte-copies of the real repo files. Edit the source file, then re-embed and
verify with the tools in scripts/ - never hand-edit a payload inside
install.sh.
- RustDesk and rustdesk-server - the excellent open-source remote desktop this installs.
- scMerlin and the AMTM-OSR / JackYaz addon family - the native webUI mount patterns (menuTree handling, page slots, addon locking) this project's webUI page follows.
- everstu/Koolcenter_rustdesk
- prior art: a RustDesk server addon for the koolshare ASUS ecosystem.