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
This commit is contained in:
rustdesk
2026-08-07 11:55:27 +08:00
parent a992c646bf
commit eed7052d1d
2 changed files with 64 additions and 10 deletions
+3 -9
View File
@@ -29,13 +29,10 @@ message PunchHoleRequest {
int32 upnp_port = 9; int32 upnp_port = 9;
bytes socket_addr_v6 = 10; bytes socket_addr_v6 = 10;
string switch_code = 11; 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; 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 { message ControlPermissions {
@@ -72,9 +69,6 @@ message PunchHole {
ControlPermissions control_permissions = 8; ControlPermissions control_permissions = 8;
ControlledContext controlled_context = 9; ControlledContext controlled_context = 9;
string webrtc_sdp_offer = 10; 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 { message TestNatRequest {
+61 -1
View File
@@ -186,6 +186,11 @@ impl WebRTCStream {
format!("webrtc://{}", encoded_sdp) 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] #[inline]
fn get_key_for_sdp(sdp: &RTCSessionDescription) -> ResultType<String> { fn get_key_for_sdp(sdp: &RTCSessionDescription) -> ResultType<String> {
let binding = sdp.unmarshal()?; let binding = sdp.unmarshal()?;
@@ -601,7 +606,21 @@ impl WebRTCStream {
#[inline] #[inline]
pub async fn get_local_endpoint_trickle(&self) -> ResultType<String> { pub async fn get_local_endpoint_trickle(&self) -> ResultType<String> {
if let Some(local_desc) = self.pc.local_description().await { 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); let endpoint = Self::sdp_to_endpoint(&sdp);
Ok(endpoint) Ok(endpoint)
} else { } 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::<serde_json::Value>(&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] #[inline]
pub async fn set_remote_endpoint(&self, endpoint: &str) -> ResultType<()> { pub async fn set_remote_endpoint(&self, endpoint: &str) -> ResultType<()> {
let offer = Self::get_remote_offer(endpoint)?; 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::<webrtc::peer_connection::sdp::session_description::RTCSessionDescription>(&sdp_json)
.expect("extra envelope key must not break RTCSessionDescription parsing");
}
#[test] #[test]
fn test_webrtc_session_key() { fn test_webrtc_session_key() {
let mut sdp_str = "".to_owned(); let mut sdp_str = "".to_owned();