DEV Community

T. Wrangler
T. Wrangler

Posted on

I reimplemented the RustDesk wire protocol in pure Go — here's what it took

RustDesk's public server now requires login, and the only official client is a desktop GUI. If you wanted to script a remote machine, pull a file, or set up a tunnel from your terminal — you were out of luck. So I built rdcli: a pure-Go CLI that speaks the RustDesk wire protocol directly. No GUI, no official binary, no Electron.

It can list remote directories, copy files both ways, open interactive or one-shot remote shells, and set up TCP tunnels through NAT — all from a terminal, with output designed for both humans and AI agents.

The protocol, briefly

RustDesk's wire protocol is a combination of protobuf messages and raw framed streams, all wrapped in NaCl crypto. Here's the pipeline I had to port:

1. Framing (BytesCodec). Every message is length-prefixed with a variable-length little-endian header: 1–4 bytes, where the bottom 2 bits encode the header length and the remaining bits encode the payload length. Simple, but easy to get subtly wrong.

2. Key exchange (NaCl box). Client and server exchange X25519 public keys, derive a shared secret, and then everything flows through secretbox with sequential nonces. The server's identity is an Ed25519-signed blob — signature-first layout, like sodiumoxide's sign::sign. Getting the signature ordering wrong means silent handshake failures.

3. Rendezvous (hbbs). The client sends a PunchHoleRequest with the peer ID, NAT type, connection type, login token, and licence key. The rendezvous server replies with relay info and the peer's socket address. This is also how online checks work (OnlineRequest/OnlineResponse).

4. Hole punching. TCP simultaneous-open: bind the same local address used for the rendezvous connection, try the peer address, time out according to NAT type — asymmetric gets a generous timeout, symmetric fails fast to relay. --relay forces the hbbr relay path: request a relay with a UUID, connect, re-key, proceed as a secure stream.

5. Login. After the secure stream is up, the client sends a Login message with flags for the connection type (port-forward, file-transfer, or terminal) plus the password. The peer replies with PeerInfo or LoginError. Passwords are hashed as sha256(sha256(pw+salt)+challenge).

6. File transfer (fs.proto). FileAction messages drive everything: ReadDir, ReadAllFiles, send/receive file lists with job IDs, directory creation, deletion. Data flows in 64KB FileTransferBlock chunks, with digest checks and resume support.

7. Terminal. TerminalAction messages with an ID, data, resize, close. The server replies with TerminalResponse output frames. Interactive mode uses a raw-mode PTY with SIGWINCH handling.

The hard parts

Protobuf without the toolchain. I vendored the .proto files from hbb_common and generated Go bindings. The generated code is committed, so builds are reproducible.

Crypto fidelity. NaCl box/secretbox with sequential nonces — the nonce scheme is where ports usually break. I ported it from crypt.rs exactly, then wrote round-trip tests against known vectors.

The desktop app's secrets. The nicest feature: rdcli import-gui reads the official app's stored login and its encrypted 00... password blobs, decrypts them the same way the GUI does, and stores everything in ~/.config/rdcli/config.toml with 0600 permissions. Your existing RustDesk setup just works.

Design decisions

  • Output contract for agents: every command emits a single JSON object on stdout; errors and progress go to stderr; exit codes are 0/1/2. No interactive prompts ever — passwords come from flags, env, config, or stdin pipes.
  • Deterministic ordering everywhere — no map iteration order leaking into tables.
  • Dependency-light: cobra, protobuf, x/crypto, x/term. That's it.

Honest limitations

  • UDP NAT traversal isn't implemented yet — punching is TCP-only, so some symmetric-NAT peers fall back to relay (relay always works).
  • No screen sharing, clipboard sync, or audio. This is a headless client.
  • Terminal requires peer ≥ 1.4.1.
  • The crypto is a faithful port but deserves independent review — please look at the handshake code.

Why AGPL-3.0?

The protocol details are ported from the AGPL-licensed RustDesk sources, so the project is AGPL-3.0. That's the honest license for this kind of work.

Try it

brew tap 4nkitd/tap
brew install rustdesk-cli

rdcli -c home-pc ls C:/Users/jane
rdcli -c home-pc cp ./backup.zip C:/Users/jane/Desktop/
rdcli -c home-pc sh "ipconfig /all"
rdcli -c home-pc tunnel -L 3389:localhost:3389
Enter fullscreen mode Exit fullscreen mode

Prebuilt binaries for linux/darwin (amd64+arm64) are on the Releases page. If you use RustDesk and ever wanted to script it — this is for you. Issues, PRs, and security reviews welcome.

Top comments (0)