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.
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.
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.
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.
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
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.