Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d4e7273
feat: add rendezvous WebRTC signaling fields
rustdesk May 17, 2026
9277af2
feat: support trickle ICE in WebRTCStream
rustdesk May 17, 2026
1998a19
fix: route WebRTC ICE without requester id
rustdesk May 18, 2026
f98f3e8
feat: WebRTC data-plane framing, DTLS binding, and pc-leak fixes
rustdesk Jul 22, 2026
0952f18
fix: preserve WebRTC endpoint and send semantics
rustdesk Jul 22, 2026
6aa8fbe
docs: webrtc 0.13 MSRV pin rationale and upgrade checklist
rustdesk Jul 26, 2026
d18dcee
feat: add LogThrottle for sites whose rate a peer controls
rustdesk Aug 6, 2026
5a45b6b
fix: cap the log file by size, and keep LogThrottle usable after pois…
rustdesk Aug 6, 2026
0a36139
fix(webrtc): reject malformed fragment framing, correct receive-path …
rustdesk Aug 6, 2026
7c4456b
proto: drop the reserved tag in PunchHole
rustdesk Aug 6, 2026
a992c64
proto: webrtc_all_ice — full-ICE offers under transport-forced relay
rustdesk Aug 6, 2026
eed7052
webrtc: declare the ICE policy inside the offer envelope, not a proto…
rustdesk Aug 7, 2026
137bb36
fmt the envelope-marker test
rustdesk Aug 7, 2026
24ae0c4
config: OPTION_ENABLE_WEBRTC, defaulted like the punch options
rustdesk Aug 7, 2026
0d2ca8a
log_throttle: add throttled_log!, the general per-call-site form
rustdesk Aug 7, 2026
a0d995f
webrtc: detach teardown, bound reassembly before growing, vet the dat…
rustdesk Aug 7, 2026
dccf317
webrtc: fix the send/cache/ICE-lifetime findings; bound log retention…
rustdesk Aug 7, 2026
6677318
webrtc: make the cache guard actually apply; drop unusable ICE servers
rustdesk Aug 8, 2026
73007cb
webrtc: split the send budget, make close_webrtc uncancellable, resto…
rustdesk Aug 8, 2026
4d1b977
stream: close the peer connection on drop
rustdesk Aug 8, 2026
ddea60c
webrtc: hand whole messages out of the read buffer instead of copying…
rustdesk Aug 8, 2026
2cb8d0c
webrtc: reject data channels effectively; hand the permit to a desync…
rustdesk Aug 8, 2026
5897012
webrtc: trim the comments to AGENTS.md length
rustdesk Aug 9, 2026
1f8463d
config: add OPTION_ENABLE_KCP_CC to config::keys
rustdesk Aug 10, 2026
01ee2f4
webrtc: own every peer connection's I/O on a process-lifetime runtime
rustdesk Aug 22, 2026
db7723e
config: add OPTION_ENABLE_TCP_PUNCH
rustdesk Aug 24, 2026
3e79687
webrtc: keep ICE candidates out of the trickle offer
rustdesk Aug 24, 2026
748eefd
webrtc: address review of the trickle-offer change
rustdesk Aug 25, 2026
a96ec7f
webrtc/tests: look for a session that outlasts the window, not an idl…
rustdesk Aug 25, 2026
cc8537c
webrtc/tests: close the stream a lost cancellation hands back
rustdesk Aug 25, 2026
6b8182e
webrtc: record why WebRTCStream has no Drop
rustdesk Aug 25, 2026
7ea29ba
webrtc: stop gathering link-local IPv6 host candidates
rustdesk Aug 25, 2026
e2aa383
webrtc: report the family of the nominated ICE pair
rustdesk Aug 25, 2026
2f75365
config: name the KCP congestion-control option for what it does
rustdesk Aug 25, 2026
96933d6
webrtc: choose ICE servers by network, and expose the STUN half
rustdesk Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ rustls-pki-types = "1.11"
rustls-native-certs = "0.8"
webpki-roots = "1.0.4"
async-recursion = "1.1"
webrtc = { version = "0.14.0", optional = true }
# Pinned to 0.13: webrtc >=0.14 pulls sdp 0.10 / webrtc-util 0.12, which use
# usize::is_multiple_of (needs rustc >=1.87), while rustdesk CI builds with Rust 1.75
# (sciter i128 ABI pin, flutter-build.yml). Bump only after CI's Rust moves past 1.87,
# and work through the upgrade checklist at the top of src/webrtc.rs first.
webrtc = { version = "0.13.0", optional = true }
libloading = "0.8"

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
Expand All @@ -81,7 +85,7 @@ protobuf-codegen = { version = "3.7" }

[dev-dependencies]
clap = "4.5.51"
webrtc = "0.14.0"
webrtc = "0.13.0" # keep in lockstep with [dependencies] webrtc (rustc 1.75 pin, see above)

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = [
Expand Down
4 changes: 4 additions & 0 deletions protos/message.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ message VideoFrame {
message IdPk {
string id = 1;
bytes pk = 2;
// DTLS certificate fingerprint of the signer's WebRTC endpoint, signed together with id/pk so
// a WebRTC peer's DTLS channel can be bound to its verified identity (defeats a rendezvous/relay
// that swaps SDP fingerprints). Empty for non-WebRTC handshakes.
string dtls_fingerprint = 3;
}

message DisplayInfo {
Expand Down
16 changes: 16 additions & 0 deletions protos/rendezvous.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ message PunchHoleRequest {
int32 upnp_port = 9;
bytes socket_addr_v6 = 10;
string switch_code = 11;
// The offer's envelope declares its own ICE transport policy (`ice_policy` key inside
// the webrtc:// payload): under force_relay it tells the peer whether the relay is
// transport-forced (WebSocket — answer may use full ICE) or policy (Relay-only + TURN).
string webrtc_sdp_offer = 12;
}

message ControlPermissions {
Expand Down Expand Up @@ -64,6 +68,7 @@ message PunchHole {
bytes socket_addr_v6 = 7;
ControlPermissions control_permissions = 8;
ControlledContext controlled_context = 9;
string webrtc_sdp_offer = 10;
}

message TestNatRequest {
Expand All @@ -90,6 +95,7 @@ message PunchHoleSent {
string version = 5;
int32 upnp_port = 6;
bytes socket_addr_v6 = 7;
string webrtc_sdp_answer = 8;
}

message RegisterPk {
Expand Down Expand Up @@ -135,6 +141,7 @@ message PunchHoleResponse {
bool is_udp = 9;
int32 upnp_port = 10;
bytes socket_addr_v6 = 11;
string webrtc_sdp_answer = 12;
}

message ConfigUpdate {
Expand Down Expand Up @@ -169,6 +176,7 @@ message RelayResponse {
int32 feedback = 9;
bytes socket_addr_v6 = 10;
int32 upnp_port = 11;
string webrtc_sdp_answer = 12;
}

message SoftwareUpdate { string url = 1; }
Expand Down Expand Up @@ -240,6 +248,13 @@ message HttpProxyResponse {
string error = 4;
}

message IceCandidate {
string id = 1;
bytes socket_addr = 2;
string session_key = 3;
string candidate = 4;
}

message RendezvousMessage {
oneof union {
RegisterPeer register_peer = 6;
Expand All @@ -265,5 +280,6 @@ message RendezvousMessage {
HealthCheck hc = 26;
HttpProxyRequest http_proxy_request = 27;
HttpProxyResponse http_proxy_response = 28;
IceCandidate ice_candidate = 29;
}
}
6 changes: 4 additions & 2 deletions src/bytes_codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use tokio_util::codec::{Decoder, Encoder};

// Bound speculative allocation from untrusted frame headers.
const MAX_PREALLOCATED_PAYLOAD_LEN: usize = 256 * 1024;
/// Largest payload representable by the four-byte RustDesk frame header.
pub const MAX_FRAME_LENGTH: usize = 0x3FFF_FFFF;

#[derive(Debug, Clone, Copy)]
pub struct BytesCodec {
Expand Down Expand Up @@ -132,7 +134,7 @@ impl Encoder<Bytes> for BytesCodec {
let h = (data.len() << 2) as u32 | 0x2;
buf.put_u16_le((h & 0xFFFF) as u16);
buf.put_u8((h >> 16) as u8);
} else if data.len() <= 0x3FFFFFFF {
} else if data.len() <= MAX_FRAME_LENGTH {
buf.put_u32_le((data.len() << 2) as u32 | 0x3);
} else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
Expand Down Expand Up @@ -290,7 +292,7 @@ mod tests {
fn decode_large_frame_header_caps_preallocation() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let n = 0x3FFFFFFFusize;
let n = MAX_FRAME_LENGTH;
const MAX_REASONABLE_CAPACITY: usize = MAX_PREALLOCATED_PAYLOAD_LEN * 4;

buf.put_u32_le((n << 2) as u32 | 0x3);
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2993,8 +2993,11 @@ pub mod keys {
"allow-command-line-settings-when-settings-disabled";

// Connection punch-through options
pub const OPTION_ENABLE_TCP_PUNCH: &str = "enable-tcp-punch";
pub const OPTION_ENABLE_UDP_PUNCH: &str = "enable-udp-punch";
pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-punch";
pub const OPTION_ENABLE_WEBRTC: &str = "enable-webrtc";
pub const OPTION_ALLOW_KCP_CC: &str = "allow-kcp-congestion-control";
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
Expand Down Expand Up @@ -3124,8 +3127,10 @@ pub mod keys {
OPTION_ALLOW_AUTO_RECORD_OUTGOING,
OPTION_HIDE_RECORDING_BUTTON,
OPTION_VIDEO_SAVE_DIRECTORY,
OPTION_ENABLE_TCP_PUNCH,
OPTION_ENABLE_UDP_PUNCH,
OPTION_ENABLE_IPV6_PUNCH,
OPTION_ENABLE_WEBRTC,
OPTION_TOUCH_MODE,
OPTION_SHOW_VIRTUAL_MOUSE,
OPTION_SHOW_VIRTUAL_JOYSTICK,
Expand Down Expand Up @@ -3192,6 +3197,7 @@ pub mod keys {
OPTION_ALLOW_INSECURE_TLS_FALLBACK,
OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS,
OPTION_ALLOW_AUTO_UPDATE,
OPTION_ALLOW_KCP_CC,
];

// BUILDIN_SETTINGS
Expand Down
13 changes: 12 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub use toml;
pub use uuid;
pub mod fingerprint;
pub use flexi_logger;
pub mod log_throttle;
pub mod stream;
pub mod websocket;
#[cfg(feature = "webrtc")]
Expand Down Expand Up @@ -457,8 +458,18 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
})
.format(opt_format)
.rotate(
Criterion::Age(Age::Day),
// Size as well as age: rotating only daily lets one day's file grow
// without limit, so whoever can drive a hot log site — a peer sending
// malformed packets, a socket erroring in a retry loop — decides how
// much disk this uses. Bounding it here covers every call site at once.
Criterion::AgeOrSize(Age::Day, 16 * 1024 * 1024),
Naming::Timestamps,
// Unchanged at 31, which is ~31 days for any machine that stays under the
// size criterion — i.e. every ordinary install, on every platform this
// ships to. Raising it to protect the flood case would have bought little
// (a count cannot outrun a flood; only the rate limits at the log sites
// can) at the price of multiplying steady-state retention and disk for
// everyone.
Cleanup::KeepLogFiles(31),
)
.start()
Expand Down
154 changes: 154 additions & 0 deletions src/log_throttle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use std::sync::Mutex;
use std::time::{Duration, Instant};

/// Collapses a log site whose call rate is set by someone else — a peer's message rate, or a
/// retry loop — into at most one line per interval.
///
/// Debug output is written to the log file, so a site that fires per received packet lets a
/// peer decide how much a machine writes to disk. Dropping the line entirely instead would
/// hide real faults, so keep one line per interval and carry the count of everything
/// suppressed since the last one.
///
/// Prefer the [`throttled_log!`](crate::throttled_log) macro, which declares the static for
/// you. Reach for this type directly only when the count belongs somewhere other than the end
/// of the line, or when the decision drives more than a log call.
///
/// Declare one per site (they do not share counts):
///
/// ```ignore
/// static DROPPED_ICE: LogThrottle = LogThrottle::new(Duration::from_secs(60));
///
/// if let Some(n) = DROPPED_ICE.due() {
/// log::debug!("dropped {n} ICE candidate(s) with no route");
/// }
/// ```
Comment on lines +18 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Identify the crate name and inspect the documented example.
fd -a '^Cargo\.toml$' -x sh -c 'echo "--- $1"; rg -n "^(name|rust-version|edition)\s*=" "$1"' sh {} \;
sed -n '1,24p' src/log_throttle.rs

Repository: rustdesk/hbb_common

Length of output: 1116


🌐 Web query:

Rustdoc code-block languages ignore no_compile_check # no_run compile_fail

💡 Result:

There is no official code-block attribute named no_compile_check in Rustdoc [1][2][3]. If you are looking for ways to control how Rustdoc handles code blocks, the following standard attributes are available: 1. ignore: Tells Rustdoc to ignore the code block entirely when running tests [2][3]. While the code will not be tested (neither compiled nor run), it will still be syntax-highlighted as Rust code if you specify rust [4]. 2. no_run: Compiles the code but does not execute it [2][3]. This is useful for code that might be unsafe, requires network access, or demonstrates behaviors that cannot be run in a standard test environment [2]. 3. compile_fail: Asserts that the code should fail to compile [2][3]. If the code compiles successfully, the test will fail [2]. If you intended to write a code block that should not be interpreted as Rust code at all (and thus not tested), you should use the text language identifier instead of rust [2]. For example: text This block is treated as text and will not be tested by rustdoc. There have been discussions within the Rust community regarding a potential no_compile attribute to distinguish between "ignored" (but still compiled) and "do not compile" states, but this has not been implemented as a standard code-block attribute [5][6].

Citations:


🌐 Web query:

doc test ignored rust code still compiled not linted

💡 Result:

In Rust documentation tests, annotating a code block with the ignore attribute (e.g., ignore) instructs the test runner to skip execution [1][6]. However, this does not mean the code is completely ignored by the toolchain: 1. Compilation and Linting: By design, the ignore attribute does not prevent the code from being parsed or linted by rustdoc [1][2][4]. Because rustdoc must still parse the block to identify it as a code example, it will still trigger certain lints, such as rustdoc::invalid_rust_codeblocks, if the content contains invalid syntax [9][10]. The block is not treated as a standard, runnable test, but it is still subject to the initial processing phases of the documentation test pipeline [2][4]. 2. Behavior with --ignored: It is a common point of confusion that code blocks marked with ignore are still technically included in the test suite [2][4]. When running cargo test, they are reported as ignored [3][7]. If you explicitly run the test suite with the --ignored flag (e.g., cargo test --doc -- --ignored), the runner may attempt to compile and execute them, depending on the current version of the toolchain and the specific configuration [2][4][7]. 3. Alternatives: Because ignore is often used for non-compilable snippets (like pseudocode or documentation that should not be tested), users frequently request a way to truly "disable" a block [2][4]. Official recommendations for such cases include: - Using the text language tag instead of rust (e.g., text) if you do not want the code to be treated as Rust code at all [1][2]. - Using hidden lines (#) to make the snippet compilable while showing only the relevant parts to the reader [1][3]. - If you need to ensure code does not compile, use the compile_fail attribute, though this still subjects the code to the compilation process [1][2].

Citations:


Make the Rustdoc example compilable or use a plain text block.

The ignored example still shows unresolved Duration and LogThrottle, so a caller who copies it into another module cannot compile it. Add Duration, LogThrottle, and hbb_common:: as needed, then drop ignore; or use text if the example is only illustrative.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log_throttle.rs` around lines 14 - 20, The Rustdoc example for
LogThrottle currently cannot compile because its referenced types are
unresolved. Update the example to import or qualify Duration, LogThrottle, and
hbb_common as needed, then remove the ignore marker so rustdoc can validate it;
use a plain text block instead only if it is not intended as executable
documentation.

pub struct LogThrottle {
interval: Duration,
state: Mutex<ThrottleState>,
}

struct ThrottleState {
suppressed: u64,
last: Option<Instant>,
}

impl LogThrottle {
pub const fn new(interval: Duration) -> Self {
Self {
interval,
state: Mutex::new(ThrottleState {
suppressed: 0,
last: None,
}),
}
}

/// Record one occurrence. Returns the number of occurrences to report (including this one)
/// when a line is due, or `None` while still inside the interval.
///
/// The first occurrence after a quiet period always reports, so an isolated fault is not
/// delayed by the interval.
pub fn due(&self) -> Option<u64> {
// A poisoned lock only means some other thread panicked while holding it; the guarded
// data is two plain counters that are still usable, and going silent for the rest of
// the process would be worse than a stale count.
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
state.suppressed += 1;
// `map_or(true, ..)` rather than clippy's preferred `is_none_or`: that was stabilized in
// Rust 1.82 and this crate builds on the 1.75 pinned by CI.
#[allow(clippy::unnecessary_map_or)]
let due = state
.last
.map_or(true, |last| last.elapsed() >= self.interval);
if !due {
return None;
}
state.last = Some(Instant::now());
Some(std::mem::replace(&mut state.suppressed, 0))
}
}

/// Log at most one line per interval from this call site, suffixed with the number of
/// occurrences it stands for.
///
/// Each expansion declares its own hidden static, so two sites never share a count and
/// adding one is a single line:
///
/// ```ignore
/// throttled_log!(Duration::from_secs(5), warn, "rejected ipc peer {peer_pid:?}");
/// ```
///
/// An isolated event logs unchanged; a burst collapses to `... (x47)`. The count includes
/// the occurrence being reported, so it reads as a total rather than as "and N more".
#[macro_export]
macro_rules! throttled_log {
($interval:expr, $level:ident, $($arg:tt)+) => {{
static THROTTLE: $crate::log_throttle::LogThrottle =
$crate::log_throttle::LogThrottle::new($interval);
if let Some(n) = THROTTLE.due() {
if n > 1 {
$crate::log::$level!("{} (x{})", format_args!($($arg)+), n);
} else {
$crate::log::$level!("{}", format_args!($($arg)+));
}
}
}};
}

#[cfg(test)]
mod tests {
use super::*;

// Two directions of one socket need two throttles: an ICMP error on a connected socket is
// reported once and cleared, so the steady state alternates (send succeeds, the next recv
// reports it) and anything shared between them is reset by the succeeding side every cycle.
#[test]
fn separate_throttles_do_not_reset_each_other() {
let send = LogThrottle::new(Duration::from_secs(60));
let recv = LogThrottle::new(Duration::from_secs(60));
assert_eq!(recv.due(), Some(1));
for _ in 0..1_000 {
// The send side succeeding must not hand the recv side a fresh emit slot.
assert_eq!(recv.due(), None);
}
assert_eq!(
send.due(),
Some(1),
"the other direction keeps its own slot"
);
}

#[test]
fn first_call_reports_immediately() {
let t = LogThrottle::new(Duration::from_secs(60));
assert_eq!(t.due(), Some(1));
}

#[test]
fn calls_inside_the_interval_are_counted_not_reported() {
let t = LogThrottle::new(Duration::from_secs(60));
assert_eq!(t.due(), Some(1));
for _ in 0..100 {
assert_eq!(t.due(), None);
}
}

#[test]
fn the_next_due_line_carries_everything_suppressed() {
let t = LogThrottle::new(Duration::ZERO);
assert_eq!(t.due(), Some(1));
// A zero interval is always due, so each call reports exactly itself.
assert_eq!(t.due(), Some(1));

let t = LogThrottle::new(Duration::from_millis(30));
assert_eq!(t.due(), Some(1));
assert_eq!(t.due(), None);
assert_eq!(t.due(), None);
std::thread::sleep(Duration::from_millis(40));
// The two suppressed calls plus this one.
assert_eq!(t.due(), Some(3));
}
}
Loading