Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
44784d9
feat: add rendezvous WebRTC signaling fields
rustdesk May 17, 2026
3426f4a
feat: support trickle ICE in WebRTCStream
rustdesk May 17, 2026
a6367b2
fix: route WebRTC ICE without requester id
rustdesk May 18, 2026
96469ae
feat: WebRTC data-plane framing, DTLS binding, and pc-leak fixes
rustdesk Jul 22, 2026
0d3e581
fix: preserve WebRTC endpoint and send semantics
rustdesk Jul 22, 2026
dd202f9
docs: webrtc 0.13 MSRV pin rationale and upgrade checklist
rustdesk Jul 26, 2026
e96a3e6
feat: add LogThrottle for sites whose rate a peer controls
rustdesk Aug 6, 2026
af964da
fix: cap the log file by size, and keep LogThrottle usable after pois…
rustdesk Aug 6, 2026
777c225
fix(webrtc): reject malformed fragment framing, correct receive-path …
rustdesk Aug 6, 2026
828e7dd
proto: drop the reserved tag in PunchHole
rustdesk Aug 6, 2026
a270f5b
proto: webrtc_all_ice — full-ICE offers under transport-forced relay
rustdesk Aug 6, 2026
bd28c58
webrtc: declare the ICE policy inside the offer envelope, not a proto…
rustdesk Aug 7, 2026
9749657
fmt the envelope-marker test
rustdesk Aug 7, 2026
dfb9b2e
config: OPTION_ENABLE_WEBRTC, defaulted like the punch options
rustdesk Aug 7, 2026
28babf4
log_throttle: add throttled_log!, the general per-call-site form
rustdesk Aug 7, 2026
46eb969
webrtc: detach teardown, bound reassembly before growing, vet the dat…
rustdesk Aug 7, 2026
54daf0a
webrtc: fix the send/cache/ICE-lifetime findings; bound log retention…
rustdesk Aug 7, 2026
40c3a29
webrtc: make the cache guard actually apply; drop unusable ICE servers
rustdesk Aug 8, 2026
5b3defc
webrtc: split the send budget, make close_webrtc uncancellable, resto…
rustdesk Aug 8, 2026
b0b624d
stream: close the peer connection on drop
rustdesk Aug 8, 2026
3ee55cc
webrtc: hand whole messages out of the read buffer instead of copying…
rustdesk Aug 8, 2026
13c20f2
webrtc: reject data channels effectively; hand the permit to a desync…
rustdesk Aug 8, 2026
2e17591
webrtc: trim the comments to AGENTS.md length
rustdesk Aug 9, 2026
2924366
config: add OPTION_ENABLE_KCP_CC to config::keys
rustdesk Aug 10, 2026
da09a1d
webrtc: own every peer connection's I/O on a process-lifetime runtime
rustdesk Aug 22, 2026
a879070
config: add OPTION_ENABLE_TCP_PUNCH
rustdesk Aug 24, 2026
ef0c201
webrtc: keep ICE candidates out of the trickle offer
rustdesk Aug 24, 2026
8eb7a88
webrtc: address review of the trickle-offer change
rustdesk Aug 25, 2026
76c4106
webrtc/tests: look for a session that outlasts the window, not an idl…
rustdesk Aug 25, 2026
1313f6d
webrtc/tests: close the stream a lost cancellation hands back
rustdesk Aug 25, 2026
b107e54
webrtc: record why WebRTCStream has no Drop
rustdesk Aug 25, 2026
f5b8009
webrtc: stop gathering link-local IPv6 host candidates
rustdesk Aug 25, 2026
16d9c27
webrtc: report the family of the nominated ICE pair
rustdesk Aug 25, 2026
3f649cf
config: name the KCP congestion-control option for what it does
rustdesk Aug 25, 2026
d336fa8
webrtc: choose ICE servers by network, and expose the STUN half
rustdesk Aug 25, 2026
bb64f3b
webrtc: evict a closing session by pc identity, without awaiting unde…
rustdesk Sep 1, 2026
4479f7c
webrtc: send over SCTP without a congestion window, as KCP does
rustdesk Sep 2, 2026
baecf86
webrtc: decode TURN userinfo before handing it to the ICE agent
rustdesk Sep 2, 2026
be5f38b
webrtc: add an answerer example for the web client's e2e
rustdesk Sep 2, 2026
9f67872
webrtc: end the ICE forwarder at gathering complete, keep the reassem…
rustdesk Sep 3, 2026
518cf47
log: quiet the two webrtc-rs sites that report the race's normal outcome
rustdesk Sep 4, 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
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ thiserror = "1.0"
httparse = "1.10"
base64 = "0.22"
url = "2.5"
percent-encoding = "2"
sha2 = "0.10"
whoami = "1.5"

Expand All @@ -68,7 +69,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 +86,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
80 changes: 80 additions & 0 deletions examples/webrtc_echo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Temporary e2e helper for the web client: answers a browser offer read from
// stdin, bridges ICE candidates over stdin/stdout as JSON lines, then echoes
// every message it receives.
extern crate hbb_common;

#[cfg(feature = "webrtc")]
#[hbb_common::tokio::main]
async fn main() -> hbb_common::anyhow::Result<()> {
use hbb_common::{
anyhow::anyhow,
serde_json::{self, json, Value},
tokio,
webrtc::WebRTCStream,
};
use std::io::BufRead;

let mut offer = String::new();
std::io::stdin().lock().read_line(&mut offer)?;
if offer.trim().is_empty() {
return Err(anyhow!("no offer"));
}
let mut stream = WebRTCStream::new(offer.trim(), false, 30000).await?;
let out = json!({
"answer": stream.local_endpoint(),
"session_key": stream.session_key(),
"local_fingerprint": stream.local_dtls_fingerprint().await?,
"remote_fingerprint": stream.remote_dtls_fingerprint().await?,
});
println!("{}", out);
let mut ice_rx = stream
.take_local_ice_rx()
.ok_or_else(|| anyhow!("no ice rx"))?;
tokio::spawn(async move {
while let Some(c) = ice_rx.recv().await {
println!("{}", json!({ "candidate": c }));
}
});
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
std::thread::spawn(move || {
let stdin = std::io::stdin();
for line in stdin.lock().lines().flatten() {
if tx.send(line).is_err() {
break;
}
}
});
let for_ice = stream.clone();
tokio::spawn(async move {
while let Some(line) = rx.recv().await {
let Ok(v) = serde_json::from_str::<Value>(&line) else {
continue;
};
if let Some(c) = v.get("candidate").and_then(Value::as_str) {
if let Err(e) = for_ice.add_remote_ice_candidate(c).await {
eprintln!("add_remote_ice_candidate failed: {e}");
}
}
}
});
stream.wait_connected(30000).await?;
println!(
"{}",
json!({
"connected": true,
"relayed": stream.is_relayed().await,
"remote_ipv6": stream.is_remote_ipv6().await,
})
);
while let Some(res) = stream.next().await {
let bytes = res?;
let len = bytes.len();
stream.send_bytes(bytes.freeze()).await?;
eprintln!("echoed {len} bytes");
}
stream.close().await;
Ok(())
}

#[cfg(not(feature = "webrtc"))]
fn main() {}
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
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2994,8 +2994,12 @@ 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_ALLOW_WEBRTC_CC: &str = "allow-webrtc-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 @@ -3125,8 +3129,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 @@ -3193,6 +3199,8 @@ pub mod keys {
OPTION_ALLOW_INSECURE_TLS_FALLBACK,
OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS,
OPTION_ALLOW_AUTO_UPDATE,
OPTION_ALLOW_KCP_CC,
OPTION_ALLOW_WEBRTC_CC,
];

// BUILDIN_SETTINGS
Expand Down
22 changes: 19 additions & 3 deletions 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 @@ -429,10 +430,15 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
#[allow(unused_mut)]
let mut logger_holder: Option<flexi_logger::LoggerHandle> = None;
INIT.call_once(|| {
// webrtc-rs reports the racing design's normal outcome at warn: cancelling the transport
// that lost closes every gathered candidate one line at a time, and trickle means the
// agent always checks before it holds a pair. Both read as faults beside a connection
// that succeeded. agent_gather keeps its warn level - a STUN server it could not reach
// is the one upstream signal that explains a session which never connected.
#[cfg(debug_assertions)]
{
use env_logger::*;
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn"));
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn,webrtc_ice::agent::agent_internal=error,webrtc::peer_connection=error"));
}
#[cfg(not(debug_assertions))]
{
Expand All @@ -447,7 +453,7 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
path.push(_name);
}
use flexi_logger::*;
if let Ok(x) = Logger::try_with_env_or_str("debug,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn") {
if let Ok(x) = Logger::try_with_env_or_str("debug,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn,webrtc_ice::agent::agent_internal=error,webrtc::peer_connection=error") {
logger_holder = x
.log_to_file(FileSpec::default().directory(path))
.write_mode(if _is_async {
Expand All @@ -457,8 +463,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,
// 90 files is ~90 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(90),
)
.start()
Expand Down
Loading