This page covers the wire protocol and message flow between the RustDesk controlled host ("server") and the controlling peer ("client") after a connection has been established. It covers:
Stream abstraction and transport variantstx vs tx_video)TestDelay heartbeat and timeout mechanismFor connection establishment (NAT traversal, relay, rendezvous), see 2.4 For authentication and password exchange, see 2.5 For the full server-side Connection lifecycle and on_message dispatch, see 3.1 For the client-side Remote::io_loop, see 3.2 For the protobuf message type catalogue, see 3.4
All peer-to-peer data is carried over the Stream type from hbb_common. Stream is a unified abstraction over TCP, WebSocket, and KCP transports, using a length-delimited codec internally so that each transmitted unit maps to exactly one discrete byte slice.
Messages are serialized as protobuf using the Message type from hbb_common::message_proto:
stream.send(&msg as &Message).awaitstream.next() → Message::parse_from_bytes(&bytes)This pattern is used on the server in the main event loop src/server/connection.rs768-789 and on the client in Remote::io_loop src/client/io_loop.rs225-243
| Transport | When Selected | Identifier in Code |
|---|---|---|
| TCP (direct) | Direct LAN or punched connection | "TCP" |
| WebSocket | use_ws() config is set | "WebSocket" |
| KCP (UDP) | UDP NAT punch succeeds | "UDP" |
| Relay TCP | Direct punch fails, fallback | "Relay" |
| IPv6 UDP | IPv6 punch succeeds | "IPv6" |
The transport type is selected in Client::_start_inner src/client.rs366-627 and passed back to the UI as stream_type via handler.set_connection_type(peer.is_secured(), direct, stream_type).
Sources: src/client.rs184-230 src/client/io_loop.rs156-188 src/server/connection.rs768-795
Before any application-level Message is exchanged, the connection is secured with a symmetric key. This happens in Client::secure_connection which is called from both the relay path and the direct connect path.
The handshake proceeds as follows:
signed_id_pk from the PunchHoleResponse or RelayResponse (rendezvous server response).decode_id_pk(signed_id_pk, rs_pk) to verify the server's Ed25519-signed Curve25519 public key against the trusted root key from get_rs_pk(key).create_symmetric_key_msg.Stream using sodiumoxide::secretbox (XSalsa20/Poly1305).From this point, all frames on the stream are symmetric-key encrypted.
When connecting to the rendezvous server with a token, secure_tcp is called first to protect the token in transit src/client.rs424-428
Encryption Handshake Sequence
Sources: src/client.rs424-580 src/server.rs161-225 src/common.rs29-43
The protobuf Message uses a oneof union field. The key variants and their typical direction of flow:
| Message Variant | Direction | Purpose |
|---|---|---|
Hash | S → C | Salt and challenge for password auth |
LoginRequest | C → S | Credentials, session flags, codec support |
LoginResponse | S → C | Peer info on success, or error string |
VideoFrame | S → C | Encoded video (VP8/VP9/AV1/H264/H265) |
AudioFrame | S → C | Opus-encoded audio |
CursorData | S → C | Cursor image pixels |
CursorPosition | S → C | Cursor coordinate update |
KeyEvent | C → S | Keyboard input |
MouseEvent | C → S | Mouse move/click/scroll |
PointerDeviceEvent | C → S | Extended pointer (tablet, touch) |
TestDelay | C ↔ S | Round-trip latency probe / keepalive |
ChatMessage | C ↔ S | In-session text chat |
FileTransferMsg | C ↔ S | File transfer operations |
PermissionInfo | S → C | Permission change notifications |
Misc | C ↔ S | Container: SwitchDisplay, RefreshVideo, AudioFormat, StopService, etc. |
Sources: src/server/connection.rs29-45 src/client/io_loop.rs23-44
The server-side Connection struct uses two separate async mpsc::UnboundedSender<(Instant, Arc<Message>)> channels to isolate video delivery latency from control message delivery.
| Channel Field | Used For |
|---|---|
ConnInner::tx | All messages except video |
ConnInner::tx_video | VideoFrame and SwitchDisplay |
The routing logic lives in ConnInner's implementation of the Subscriber trait src/server/connection.rs313-339:
message::Union::VideoFrame(_) → tx_videomisc::Union::SwitchDisplay(_) → tx_videotxSwitchDisplay is routed with video to guarantee ordering: a display switch and its first video frame must arrive in sequence.
In the main event loop the corresponding receivers are consumed with tokio::select! src/server/connection.rs822-860 Stale audio frames (>1000 ms queued) are silently dropped. A StopService misc message triggers graceful teardown.
Server Channel Routing
Sources: src/server/connection.rs67-78 src/server/connection.rs313-339 src/server/connection.rs822-860 src/server.rs102-158
TestDelay is used for two purposes: keeping the connection alive and measuring round-trip latency.
Constants src/server/connection.rs341-347:
| Constant | Value | Role |
|---|---|---|
TEST_DELAY_TIMEOUT | 1 second | How often server sends a probe |
SEC30 | 30 seconds | Client-side timeout threshold |
SESSION_TIMEOUT | 30 seconds | Session inactivity limit |
Flow:
test_delay_timer fires every TEST_DELAY_TIMEOUT on the server src/server/connection.rs517-518TestDelay { id, time: now } to the client.handle_test_delay in src/client.rs.network_delay from the round-trip time and updates last_test_delay.last_recv_time on the client is updated on every received message. If SEC30 elapses with no message, the client disconnects src/client/io_loop.rs267-270TestDelay Sequence
Sources: src/server/connection.rs341-347 src/server/connection.rs517-519 src/client/io_loop.rs266-270
Independent of the peer stream, the server process runs a background sync loop with the HBBS (rendezvous) server. This is implemented in src/hbbs_http/sync.rs and is not part of the peer-to-peer channel.
| Constant | Value | Purpose |
|---|---|---|
TIME_HEARTBEAT | 15 seconds | Interval for heartbeat HTTP POSTs to HBBS |
UPLOAD_SYSINFO_TIMEOUT | 120 seconds | Timeout for system info upload |
TIME_CONN | 3 seconds | Connection-status check interval |
RendezvousMediator::start_all() calls crate::hbbs_http::sync::start() src/rendezvous_mediator.rs67 This initializes the SENDER lazy static, which spawns start_hbbs_sync_async() in a dedicated OS thread. The function returns a broadcast::Sender<Vec<i32>> that carries lists of connection IDs to forcibly close.
ConnectionEach Connection subscribes at startup src/server/connection.rs376:
In the main loop, HBBS close signals are consumed src/server/connection.rs815-821:
This is the mechanism used by the web management console to terminate any active session.
HBBS Sync Architecture
Sources: src/hbbs_http/sync.rs1-90 src/rendezvous_mediator.rs60-100 src/server/connection.rs376 src/server/connection.rs815-821
On the client side, Remote::io_loop in src/client/io_loop.rs drives all communication after Client::start returns a connected Stream. The loop uses tokio::select! over five concurrent sources src/client/io_loop.rs223-320:
| Select Branch | Handler | Purpose |
|---|---|---|
peer.next() | handle_msg_from_peer() | Incoming protobuf frames from server |
self.receiver.recv() | handle_msg_from_ui() | Data enum commands from UI/session layer |
rx_clip_client.recv() | handle_local_clipboard_msg() | Local clipboard changes to sync |
self.timer.tick() | file job polling + 30s timeout | File transfer drive, dead connection detection |
status_timer.tick() | update_quality_status() | FPS, throughput, codec UI update |
The Data enum in src/client.rs is the in-process command channel from the UI into the I/O loop. Key variants include Data::Message(msg) (send a protobuf message), Data::RecordScreen(bool), Data::TakeScreenshot, and file transfer operations.
Client I/O Loop — Select Sources Mapped to Code Entities
Sources: src/client/io_loop.rs133-321 src/client/io_loop.rs56-81 src/flutter_ffi.rs628-641 src/flutter.rs44-46
The Stream send timeout is configured at the start of each Connection src/server/connection.rs521-527:
| Connection Mode | Constant | Value |
|---|---|---|
| Remote desktop | SEND_TIMEOUT_VIDEO | 12,000 ms |
| File transfer / port forward / terminal | SEND_TIMEOUT_OTHER | 120,000 ms |
SEND_TIMEOUT_OTHER = SEND_TIMEOUT_VIDEO * 10 = 120 seconds src/server/connection.rs345-346
In the rx.recv() branch of the server loop, audio frames with a queue age exceeding 1000 ms are silently discarded to prevent buildup during a slow network event src/server/connection.rs838-846
Sources: src/server/connection.rs341-347 src/server/connection.rs521-527 src/server/connection.rs833-846
Server → Client (video path)
Client → Server (input path)
Sources: src/server/connection.rs313-339 src/server/connection.rs822-832 src/client/io_loop.rs224-260 src/flutter_ffi.rs568-606 src/flutter.rs217-225
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.