{"slug": "i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-here-s-what-it-took", "title": "I reimplemented the RustDesk wire protocol in pure Go — here's what it took", "summary": "A developer has built rdcli, a pure-Go CLI that implements the RustDesk wire protocol, enabling terminal-based remote access without the official GUI. The tool supports file transfer, remote shells, and TCP tunnels, and can import credentials from the official RustDesk app. The project is released under AGPL-3.0.", "body_md": "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`\n\n: a pure-Go CLI that speaks the RustDesk wire protocol directly. No GUI, no official binary, no Electron.\n\nIt 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.\n\nRustDesk'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:\n\n**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.\n\n**2. Key exchange (NaCl box).** Client and server exchange X25519 public keys, derive a shared secret, and then everything flows through `secretbox`\n\nwith sequential nonces. The server's identity is an Ed25519-signed blob — signature-first layout, like sodiumoxide's `sign::sign`\n\n. Getting the signature ordering wrong means silent handshake failures.\n\n**3. Rendezvous (hbbs).** The client sends a `PunchHoleRequest`\n\nwith 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`\n\n/`OnlineResponse`\n\n).\n\n**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`\n\nforces the hbbr relay path: request a relay with a UUID, connect, re-key, proceed as a secure stream.\n\n**5. Login.** After the secure stream is up, the client sends a `Login`\n\nmessage with flags for the connection type (port-forward, file-transfer, or terminal) plus the password. The peer replies with `PeerInfo`\n\nor `LoginError`\n\n. Passwords are hashed as `sha256(sha256(pw+salt)+challenge)`\n\n.\n\n**6. File transfer (fs.proto).** `FileAction`\n\nmessages drive everything: ReadDir, ReadAllFiles, send/receive file lists with job IDs, directory creation, deletion. Data flows in 64KB `FileTransferBlock`\n\nchunks, with digest checks and resume support.\n\n**7. Terminal.** `TerminalAction`\n\nmessages with an ID, data, resize, close. The server replies with `TerminalResponse`\n\noutput frames. Interactive mode uses a raw-mode PTY with SIGWINCH handling.\n\n**Protobuf without the toolchain.** I vendored the `.proto`\n\nfiles from hbb_common and generated Go bindings. The generated code is committed, so builds are reproducible.\n\n**Crypto fidelity.** NaCl box/secretbox with sequential nonces — the nonce scheme is where ports usually break. I ported it from `crypt.rs`\n\nexactly, then wrote round-trip tests against known vectors.\n\n**The desktop app's secrets.** The nicest feature: `rdcli import-gui`\n\nreads the official app's stored login and its encrypted `00...`\n\npassword blobs, decrypts them the same way the GUI does, and stores everything in `~/.config/rdcli/config.toml`\n\nwith 0600 permissions. Your existing RustDesk setup just works.\n\nThe 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.\n\n```\nbrew tap 4nkitd/tap\nbrew install rustdesk-cli\n\nrdcli -c home-pc ls C:/Users/jane\nrdcli -c home-pc cp ./backup.zip C:/Users/jane/Desktop/\nrdcli -c home-pc sh \"ipconfig /all\"\nrdcli -c home-pc tunnel -L 3389:localhost:3389\n```\n\nPrebuilt 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.", "url": "https://wpnews.pro/news/i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-here-s-what-it-took", "canonical_source": "https://dev.to/ttywrangler/i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-heres-what-it-took-2kok", "published_at": "2026-08-16 16:46:55+00:00", "updated_at": "2026-08-16 17:42:18.864615+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["RustDesk", "rdcli", "Go", "NaCl", "X25519", "Ed25519", "AGPL-3.0", "4nkitd"], "alternates": {"html": "https://wpnews.pro/news/i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-here-s-what-it-took", "markdown": "https://wpnews.pro/news/i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-here-s-what-it-took.md", "text": "https://wpnews.pro/news/i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-here-s-what-it-took.txt", "jsonld": "https://wpnews.pro/news/i-reimplemented-the-rustdesk-wire-protocol-in-pure-go-here-s-what-it-took.jsonld"}}