From eed7052d1d03fcc9b74becce97f753448542127b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 7 Aug 2026 11:55:27 +0800 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ --- protos/rendezvous.proto | 12 ++------ src/webrtc.rs | 62 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/protos/rendezvous.proto b/protos/rendezvous.proto index d5e9e0956..8f796502f 100644 --- a/protos/rendezvous.proto +++ b/protos/rendezvous.proto @@ -29,13 +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; - // The attached offer gathers every ICE candidate type (host/srflx/relay), so a direct - // WebRTC path may form even when force_relay is set. Sent by clients whose force_relay - // stems from the transport (WebSocket tunnels only the signaling/relay legs and kills - // classic punching, not ICE) rather than from relay-by-policy; absent/false keeps the - // old semantics where force_relay implies a Relay-only-ICE offer. - bool webrtc_all_ice = 13; } message ControlPermissions { @@ -72,9 +69,6 @@ message PunchHole { ControlPermissions control_permissions = 8; ControlledContext controlled_context = 9; string webrtc_sdp_offer = 10; - // Forwarded from PunchHoleRequest.webrtc_all_ice; see that field. Tag 11 previously - // carried the never-shipped `requester_id` (added and removed in one unshipped batch). - bool webrtc_all_ice = 11; } message TestNatRequest { diff --git a/src/webrtc.rs b/src/webrtc.rs index aedfdd819..0077914d8 100644 --- a/src/webrtc.rs +++ b/src/webrtc.rs @@ -186,6 +186,11 @@ impl WebRTCStream { format!("webrtc://{}", encoded_sdp) } + // Envelope JSON key carrying the local description's ICE transport policy, alongside the + // RTCSessionDescription fields (see `get_local_endpoint_trickle`). + const ICE_POLICY_KEY: &str = "ice_policy"; + const ICE_POLICY_ALL: &str = "all"; + #[inline] fn get_key_for_sdp(sdp: &RTCSessionDescription) -> ResultType { let binding = sdp.unmarshal()?; @@ -601,7 +606,21 @@ impl WebRTCStream { #[inline] pub async fn get_local_endpoint_trickle(&self) -> ResultType { if let Some(local_desc) = self.pc.local_description().await { - let sdp = serde_json::to_string(&local_desc)?; + let sdp = if self.relay_only { + serde_json::to_string(&local_desc)? + } else { + // Declare the ICE transport policy inside the envelope. The receiver of an + // offer that arrives with force_relay set must know whether it may answer + // with full ICE (relay forced by the transport, e.g. WebSocket signaling) + // or must stay Relay-only + TURN-gated (relay by policy) — and the envelope + // is the offer's own property, so it rides here rather than in a proto + // field the rendezvous server would have to forward. An extra key is + // invisible to older peers: serde ignores unknown fields when parsing + // RTCSessionDescription, so absence — not an error — is the old semantics. + let mut v = serde_json::to_value(&local_desc)?; + v[Self::ICE_POLICY_KEY] = serde_json::Value::from(Self::ICE_POLICY_ALL); + serde_json::to_string(&v)? + }; let endpoint = Self::sdp_to_endpoint(&sdp); Ok(endpoint) } else { @@ -609,6 +628,24 @@ impl WebRTCStream { } } + /// Whether the peer's endpoint declares it was built with ICE transport policy `all` + /// (W3C RTCIceTransportPolicy), i.e. it gathers host/srflx/relay candidates and a direct + /// pair may form even though the request carries force_relay. Absent key, foreign format + /// or parse failure all mean "not declared" — the old Relay-only reading. + pub fn endpoint_declares_all_ice(endpoint: &str) -> bool { + let Ok(sdp_json) = Self::get_remote_offer(endpoint) else { + return false; + }; + serde_json::from_str::(&sdp_json) + .ok() + .and_then(|v| { + v.get(Self::ICE_POLICY_KEY)? + .as_str() + .map(|p| p == Self::ICE_POLICY_ALL) + }) + .unwrap_or(false) + } + #[inline] pub async fn set_remote_endpoint(&self, endpoint: &str) -> ResultType<()> { let offer = Self::get_remote_offer(endpoint)?; @@ -1037,6 +1074,29 @@ mod tests { ); } + #[test] + fn test_endpoint_ice_policy_declaration() { + // An envelope with the marker declares full ICE; everything else — no marker, + // wrong value, foreign scheme, garbage — reads as the old Relay-only semantics. + let marked = WebRTCStream::sdp_to_endpoint(r#"{"type":"offer","sdp":"v=0","ice_policy":"all"}"#); + assert!(WebRTCStream::endpoint_declares_all_ice(&marked)); + + let unmarked = WebRTCStream::sdp_to_endpoint(r#"{"type":"offer","sdp":"v=0"}"#); + assert!(!WebRTCStream::endpoint_declares_all_ice(&unmarked)); + + let wrong = WebRTCStream::sdp_to_endpoint(r#"{"type":"offer","sdp":"v=0","ice_policy":"relay"}"#); + assert!(!WebRTCStream::endpoint_declares_all_ice(&wrong)); + + assert!(!WebRTCStream::endpoint_declares_all_ice("")); + assert!(!WebRTCStream::endpoint_declares_all_ice("webrtc://not-base64!")); + assert!(!WebRTCStream::endpoint_declares_all_ice("https://example.com")); + + // The marker must be invisible to the plain RTCSessionDescription parse old peers do. + let sdp_json = WebRTCStream::get_remote_offer(&marked).unwrap(); + serde_json::from_str::(&sdp_json) + .expect("extra envelope key must not break RTCSessionDescription parsing"); + } + #[test] fn test_webrtc_session_key() { let mut sdp_str = "".to_owned();