Skip to content
Merged
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
6fffd88
feat: add rendezvous WebRTC signaling fields
rustdesk May 17, 2026
411d23b
feat: support trickle ICE in WebRTCStream
rustdesk May 17, 2026
1ee667e
fix: route WebRTC ICE without requester id
rustdesk May 18, 2026
e9cdb2f
feat: WebRTC data-plane framing, DTLS binding, and pc-leak fixes
rustdesk Jul 22, 2026
776e71c
fix: preserve WebRTC endpoint and send semantics
rustdesk Jul 22, 2026
59ce36f
docs: webrtc 0.13 MSRV pin rationale and upgrade checklist
rustdesk Jul 26, 2026
bf95d52
feat: add LogThrottle for sites whose rate a peer controls
rustdesk Aug 6, 2026
fcc713b
fix: cap the log file by size, and keep LogThrottle usable after pois…
rustdesk Aug 6, 2026
c4cc0b4
fix(webrtc): reject malformed fragment framing, correct receive-path …
rustdesk Aug 6, 2026
a122e8f
proto: drop the reserved tag in PunchHole
rustdesk Aug 6, 2026
adc10a5
proto: webrtc_all_ice — full-ICE offers under transport-forced relay
rustdesk Aug 6, 2026
041213e
webrtc: declare the ICE policy inside the offer envelope, not a proto…
rustdesk Aug 7, 2026
a94ba59
fmt the envelope-marker test
rustdesk Aug 7, 2026
603edc7
config: OPTION_ENABLE_WEBRTC, defaulted like the punch options
rustdesk Aug 7, 2026
402cb24
log_throttle: add throttled_log!, the general per-call-site form
rustdesk Aug 7, 2026
917907a
webrtc: detach teardown, bound reassembly before growing, vet the dat…
rustdesk Aug 7, 2026
6a175cf
webrtc: fix the send/cache/ICE-lifetime findings; bound log retention…
rustdesk Aug 7, 2026
faf1723
webrtc: make the cache guard actually apply; drop unusable ICE servers
rustdesk Aug 8, 2026
e8ec370
webrtc: split the send budget, make close_webrtc uncancellable, resto…
rustdesk Aug 8, 2026
b127621
stream: close the peer connection on drop
rustdesk Aug 8, 2026
5b030c9
webrtc: hand whole messages out of the read buffer instead of copying…
rustdesk Aug 8, 2026
b4b0920
webrtc: reject data channels effectively; hand the permit to a desync…
rustdesk Aug 8, 2026
d705a2c
webrtc: trim the comments to AGENTS.md length
rustdesk Aug 9, 2026
2bd477b
config: add OPTION_ENABLE_KCP_CC to config::keys
rustdesk Aug 10, 2026
ecbca13
webrtc: own every peer connection's I/O on a process-lifetime runtime
rustdesk Aug 22, 2026
e813e58
config: add OPTION_ENABLE_TCP_PUNCH
rustdesk Aug 24, 2026
01005b0
webrtc: keep ICE candidates out of the trickle offer
rustdesk Aug 24, 2026
2b2babe
webrtc: address review of the trickle-offer change
rustdesk Aug 25, 2026
d8bc2d1
webrtc/tests: look for a session that outlasts the window, not an idl…
rustdesk Aug 25, 2026
cc3fb9c
webrtc/tests: close the stream a lost cancellation hands back
rustdesk Aug 25, 2026
3261057
webrtc: record why WebRTCStream has no Drop
rustdesk Aug 25, 2026
f000608
webrtc: stop gathering link-local IPv6 host candidates
rustdesk Aug 25, 2026
535bf77
webrtc: report the family of the nominated ICE pair
rustdesk Aug 25, 2026
6f4eeaf
config: name the KCP congestion-control option for what it does
rustdesk Aug 25, 2026
1ad9fbd
webrtc: choose ICE servers by network, and expose the STUN half
rustdesk Aug 25, 2026
31d3789
webrtc: evict a closing session by pc identity, without awaiting unde…
rustdesk Sep 1, 2026
846daf8
webrtc: send over SCTP without a congestion window, as KCP does
rustdesk Sep 2, 2026
00f0a72
webrtc: decode TURN userinfo before handing it to the ICE agent
rustdesk Sep 2, 2026
98d256d
webrtc: add an answerer example for the web client's e2e
rustdesk Sep 2, 2026
4867575
webrtc: end the ICE forwarder at gathering complete, keep the reassem…
rustdesk Sep 3, 2026
470612b
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,9 +2994,13 @@ pub mod keys {
"allow-command-line-settings-when-settings-disabled";

// Connection punch-through / port-forward 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_PORT_FORWARD_MUX: &str = "enable-port-forward-mux";
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 @@ -3133,9 +3137,11 @@ 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_PORT_FORWARD_MUX,
OPTION_ENABLE_WEBRTC,
OPTION_TOUCH_MODE,
OPTION_SHOW_VIRTUAL_MOUSE,
OPTION_SHOW_VIRTUAL_JOYSTICK,
Expand Down Expand Up @@ -3202,6 +3208,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