56 Commits
Author SHA1 Message Date
rustdeskandClaude Fable 5.1 e74a188f4f KeyExchange: sign KxParams, a structure, in place of a fixed byte string
The server's second signature covered "rdkx-ver" || key || version, a
layout that could take nothing more. It now covers KxParams, a message
holding the key as it went out and the advertised version, so anything
the server needs signed later is a field added to it, and a client that
does not know a field parses past it, as with IdPk on the peer side.
Field 3 keeps its number under the name signed_params.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-22 17:43:38 +08:00
rustdeskandClaude Fable 5.1 5194159858 kx: drop the advertisement echo
signed_version settles the server's version inside the key exchange, so
an echo of it on the messages after has nothing left to confirm. With it
go RendezvousMessage.kx_advertised, the check the transport ran on each
decrypted frame, and the tests of both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-22 17:22:01 +08:00
rustdeskandClaude Fable 5.1 e3452acf59 kx: comments say what the fields and functions are
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-22 15:02:44 +08:00
rustdeskandClaude Fable 5.1 d9f519e915 KeyExchange: signed_version, the server's signature over its version
The echo catches an advertisement lowered in the clear only once a tagged
frame arrives, so the initiator's first frame has already gone out under
the lower version by then. A server that signs its version instead sets
bit 255 of the ephemeral X25519 key inside the signed keys[0] and puts
sign("rdkx-ver" || key || version LE) here. RFC 7748 masks that bit, so
the box opens as before on either side, and a legacy server, whose keys
are canonical, never sets it. A client that sees the bit, or the field,
verifies before trusting the version, so the pair settles it at the
handshake rather than one frame pair later.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-22 13:18:45 +08:00
rustdeskandClaude Fable 5.1 1266b56150 kx_advertised: every message after the exchange, not the first alone
The contract named the server's first message after the key exchange. That
frame sits at a position an attacker needs no key to find, so one who had
lowered the advertisement in the clear only had to drop it to be rid of
the check for the whole session. With every message carrying the echo,
being rid of it means dropping them all, which is a plain DoS.

The client already kept the check armed until an echo arrived; this is the
wording the server side is written to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-21 18:33:33 +08:00
rustdeskandClaude Fable 5.1 e272fede36 tcp: key exchange version 1, one stream key per direction
Version 1 of the key exchange splits the exchanged key into a subkey per
direction, the way Noise splits its cipher states after the handshake, and
leaves the nonce layout and the handshake messages as they are. Each subkey
is a keyed BLAKE2b over the handshake transcript: both ephemeral public keys,
the version the responder advertised and the one the initiator picked, so a
change to any of it in transit leaves the two sides with different keys. The
original scheme is version 0, which is also what an absent field means.

Negotiated through new fields old peers do not see: the side that speaks
first advertises the newest version it has (`KeyExchange.version` from the
server, `IdPk.kx_version` inside the signed identity from the controlled
side), the other side answers with the highest both have, and the pair runs
that. Any old peer on either end keeps today's stream byte for byte. The
controlled side's advertisement sits inside the signature; the server's is
repeated in `RendezvousMessage.kx_advertised` on its first message after the
exchange, inside the encrypted channel, and a client that was shown a
different value in the clear refuses the stream.

`Encrypt::new` is unchanged; `new_split`, `set_key_split` and
`check_kx_advertised` are added beside it, and `box_pk_of` for a responder
that kept only the secret half of its ephemeral pair. The tests pin the
version 0 output as well as the split and the echo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-21 18:33:33 +08:00
rustdeskandClaude Opus 5 e6fe9c3332 move the modules only rustdesk uses out to the base crate
hbb_common is shared with the server, but `fs`, `platform`, `keyboard`,
`message.proto` and most of `config::keys` are reached only from the client.
Keeping them here means every change to them costs a submodule round-trip.

They move to the new `base` crate in the rustdesk tree. What stays is
what the server actually names: `rendezvous.proto`, the socket/stream layer, the
`Config` core, and the 32 option keys this crate reads itself. Those 32 are
re-exported from `base::config::keys`, so callers still see one set.

`IdPk` moves from message.proto to rendezvous.proto. It is register-pk handshake
business, the server is its only other user, and the field numbers are unchanged
so the wire format is identical.

`config::patch()` resolves a home directory through the shell lookup that lived
in `platform::linux`, so `find_cmd_path`, `CMD_*` and `run_cmds_trim_newline`
stay behind in a new `sh` module. `base::platform::linux` re-exports
them, leaving every existing `platform::linux::CMD_SH` path working.

backtrace, filetime, osascript, smithay-client-toolkit and winapi go with the
code that used them, so the server no longer builds a Wayland client toolkit and
a Windows API crate it never calls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-08 08:53:58 +08:00
rustdeskandClaude Opus 5 041213e44c webrtc: declare the ICE policy inside the offer envelope, not a proto field
Reverts the webrtc_all_ice proto field (64b54ab) in favor of an
`ice_policy: "all"` key inside the webrtc:// envelope JSON, next to the
RTCSessionDescription fields. Same information, better carrier:

- it is a property of the offer itself, so it rides with the offer;
- the rendezvous server never has to know: the envelope is an opaque,
  length-bounded string to hbbs, so no forwarding code and no vendored
  proto copies to keep in sync;
- serde ignores unknown JSON keys when parsing RTCSessionDescription,
  so every skew combination degrades exactly like the proto field did:
  absence - not an error - is the old Relay-only reading.

endpoint_declares_all_ice() is the receiving side: parse failure,
foreign scheme or missing key all read as "not declared".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 adc10a5e0c proto: webrtc_all_ice — full-ICE offers under transport-forced relay
use_ws() folds into force_relay because a ws tunnel kills classic TCP/UDP
punching — but ICE opens its own sockets and does not care how signaling
reaches the server. Without a signal, the controlled side must treat every
force_relay offer as Relay-only ICE (answer gated on TURN), which locks
WebSocket deployments out of direct WebRTC entirely.

webrtc_all_ice marks an offer that gathered every candidate type: the
controller's force_relay covers only classic punching, not ICE policy.
Absent/false keeps today's semantics on every skew combination (old
controller, old server dropping the field, old controlled side).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 a122e8f4d5 proto: drop the reserved tag in PunchHole
`requester_id = 11` was added and removed in the same rebase batch, never
reached main, and never reached hbbs — whose vendored copy of this file still
stops at field 9. So nothing has ever written or read tag 11, and reserving it
guards a wire format that does not exist.

It was also inconsistent with what this branch already does: `IceCandidate`
retyped tag 2 from `string to_id` to `bytes socket_addr` in place, which is only
sound because none of this proto has shipped. Same premise, so tag 11 is free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 c4cc0b4429 fix(webrtc): reject malformed fragment framing, correct receive-path docs
`next()` read every non-FRAG_END header as "more fragments", so a peer whose
framing had diverged was only caught by the MAX_FRAME_LENGTH cap — and a
FRAG_MORE carrying no payload was never caught at all: it adds nothing to the
accumulator, so the cap never trips and the loop spins for as long as the peer
keeps writing, with no error and no teardown. Decide the header's meaning in one
match, so a future header kind cannot be handled in one place and missed in the
other. Neither case is reachable from send_bytes_inner, which emits FRAG_MORE
only for a full MAX_FRAGMENT_PAYLOAD chunk.

Release the accumulator on the error paths rather than truncating it: at the cap
that is ~1 GiB still referenced through the SESSIONS clone.

Doc corrections, all of them overclaims in the previous pass:

- the cancel-safety entry held only for the successful read path.
  read_data_channel does await after dequeuing on its ErrShortBuffer and DCEP
  branches, and next() awaits pc.close() on its error paths — where
  RTCPeerConnection::close latches is_closed before its first await, so a
  cancelled close silently turns every later close into a no-op and leaves the
  pc in SESSIONS.
- recv_state: cancellation drops the guard mid-message, so it is the
  single-reader assumption, not the mutex, that ultimately keeps two readers
  from splicing into one accumulator.
- is_relayed: stream.rs promised None before pair selection while webrtc.rs
  documented Some(true) under Relay policy; align both.
- get_local_endpoint: examples/webrtc.rs calls it too, not only the tests.
- PunchHole.reserved 11: named the wrong writer — PunchHole is written by the
  rendezvous server, not by peers. Reserve the name as well as the tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 4.8 e9cdb2fe5d feat: WebRTC data-plane framing, DTLS binding, and pc-leak fixes
- 1-byte-header fragmentation past the 64KB SCTP cap; empty-message and clean-EOF handling
- is_relayed() via selected candidate-pair stats for the direct/relayed flag
- IdPk.dtls_fingerprint + rendezvous webrtc SDP/IceCandidate proto fields
- fix pc leaks: Weak capture breaks the state-handler Arc self-cycle; close pc on new() error paths

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdesk 1ee667e78b fix: route WebRTC ICE without requester id 2026-09-04 23:33:31 +08:00
rustdesk 6fffd8842a feat: add rendezvous WebRTC signaling fields 2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 6918d35935 proto: PortForwardChannel family for a multiplexed port-forward tunnel
Adds PortForward.multiplex, Features.port_forward_mux and the
open/opened/data/close/window_update channel frames, union tag 33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-03 20:14:42 +08:00
RustDesk 5438d20225 switch_code 2026-07-17 17:44:01 +08:00
21pages 91b13be6fd Add controlled context for controller audit attribution
Add ControlledContext to rendezvous messages so the server can pass a controller-user audit ref to the controlled client.

The controlled client returns the ref when posting audit logs, allowing the server to associate those logs with the controller user.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-26 14:16:56 +08:00
RustDesk c8cbb6be28 Update rendezvous.proto 2026-05-13 18:06:39 +08:00
RustDesk 42af0f0aed Merge pull request #533 from fufesou/feat/terminal-retry-output
feat(terminal): flag, retry output
2026-05-07 12:08:56 +08:00
fufesou a4309b5b2b feat(terminal): flag, retry output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 23:37:43 +08:00
fufesou a24767a0ad feat: option, privacy mode
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-21 21:14:36 +08:00
RustDesk 6fb03d076e http_proxy_request 2026-03-24 20:39:19 +08:00
RustDesk 48c37de3e6 Update message.proto 2026-03-02 12:12:05 +08:00
RustDesk dea7d6cbd8 Update message.proto 2026-03-02 11:57:58 +08:00
21pages e163e2d829 add three state control permissions
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-01-07 12:42:21 +08:00
21pages 510ebdca50 Revert "add ControlPermissions"
This reverts commit 178c97d59f.
2026-01-07 12:42:01 +08:00
21pages 178c97d59f add ControlPermissions
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-01-05 22:13:56 +08:00
RustDesk 4637bf8fc1 Revert "Allow restricting remote control permissions for different users" 2026-01-05 22:06:13 +08:00
21pages 3a0034f104 controlling strategy
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-12-29 14:08:43 +08:00
fufesou ae7aaf9629 refact: proto, remove unused field
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-13 19:21:44 +08:00
fufesou 4fe91763b7 fix: Wayland resolution
1. Add filed `scale` in `message SwitchDisplay`(`message.proto`).
2. Add new function to get all display info.
3. Add help link of KDE Plasma Wayland.

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-12 23:16:04 +08:00
21pages 44e8a46e94 Revert "ab default password"
This reverts commit f9e903b762.
2025-09-04 18:26:50 +08:00
21pages 3dbe437a8c Revert "remove support_controlling_salt field in Hash"
This reverts commit 11dba932e0.
2025-09-04 18:26:29 +08:00
21pages 11dba932e0 remove support_controlling_salt field in Hash
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-04 17:18:17 +08:00
21pages f9e903b762 ab default password
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-02 11:31:11 +08:00
fufesou 29bae8a4d4 feat: opt, show my cursor
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-27 07:47:16 +08:00
RustDesk 221c2bfb3e Merge pull request #335 from fufesou/feat/clipboard_file_audit
feat: clipboard files, audit
2025-08-25 15:03:33 +08:00
fufesou f6fb16d6e7 feat: file transfer, digest flag, is_resume
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-12 07:10:34 +08:00
fufesou fee1a11f15 feat: clipboard files, audit
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-11 09:03:25 +08:00
fufesou 215b0e7700 feat: file transfer, resume
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-06 16:28:46 +08:00
fufesou 79734df69c feat: terminal, restore sessions
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-18 08:16:52 +08:00
RustDesk f850a167ac terminal persistent 2025-06-30 00:31:09 +08:00
RustDesk 117ea7c341 terminal 2025-06-29 14:08:03 +08:00
RustDesk df95f44499 more udp punch 2025-06-10 12:01:43 +08:00
RustDesk b69b097c6f kcp stream 2025-06-03 19:41:30 +08:00
RustDesk 0e279c1a8f Update rendezvous.proto 2025-05-23 21:55:40 +08:00
fufesou b08c92ad34 feat: screenshot
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-04-28 18:34:59 +08:00
fufesou 14936c8800 Remove in login request.
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-24 20:57:41 +08:00
fufesou f4cc39135c feat: remote printer, reuse fs
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-23 11:58:27 +08:00
fufesou 07d3cc5d15 Feat. Remote printer.
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-03-16 22:56:53 +08:00