mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
ws: decouple ICE policy from force_relay — full-ICE WebRTC over WebSocket
WebSocket support folds into force_relay because a ws tunnel kills classic TCP/UDP punching — but that conflated transport necessity with relay policy, and the WebRTC decisions keyed off the merged flag: a ws client built no offerer at all without TURN, and only a Relay-only-ICE one with it. ws deployments could never reach a direct WebRTC connection, which is exactly the path they are supposed to live on. Split the flag. LoginConfigHandler now tracks policy_relay (the force-always-relay option, an explicit relay request — /r ids and retry-via-relay included — and proxy) separately; force_relay stays policy_relay || use_ws() and keeps governing the classic paths, so non-ws behavior is unchanged everywhere: - the offerer's existence and ICE policy follow policy_relay: under pure ws the offer gathers every candidate type and may go direct; under relay-by-policy it stays Relay-only ICE, TURN-gated, exactly as before; - the RelayResponse race applies the prefer-P2P window under ws (a direct ICE path is worth delaying an already-ready relay for) while policy relay keeps first-success semantics; - the request carries webrtc_all_ice (hbb_common 64b54ab) so the controlled side knows the offer is full-ICE: it answers with full ICE and no TURN requirement, while offers without the bit keep today's relay-only answer path on every version-skew combination. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
This commit is contained in:
Submodule libs/hbb_common updated: 7c4456be9b...a992c646bf
@@ -499,7 +499,10 @@ impl Client {
|
||||
// When this request carries an offer, `_start_inner` keeps its rendezvous socket solely
|
||||
// for trickle signaling; a separate offer-less request owns any TCP punch attempt.
|
||||
let webrtc_offerer = if Self::should_create_webrtc_offerer(&interface) {
|
||||
match WebRTCStream::new("", interface.is_force_relay(), CONNECT_TIMEOUT).await {
|
||||
// ICE policy follows relay-by-POLICY, not force_relay: under WebSocket the
|
||||
// latter is set for the classic paths, but ICE opens its own sockets and may
|
||||
// still go direct - that is the only P2P path ws deployments have.
|
||||
match WebRTCStream::new("", interface.is_policy_relay(), CONNECT_TIMEOUT).await {
|
||||
Ok(stream) => Some(stream),
|
||||
Err(err) => {
|
||||
log::warn!("webrtc offerer setup failed: {}", err);
|
||||
@@ -604,10 +607,13 @@ impl Client {
|
||||
/// ws deployments where classic punching is forced to relay. Independent of the udp-punch
|
||||
/// option too — the offer rides any request, and a server or peer without WebRTC support
|
||||
/// drops the field, so the race simply proceeds without it.
|
||||
/// Under force_relay the pc uses Relay-only ICE, which gathers nothing and can never connect
|
||||
/// unless a TURN server is configured, so skip building a guaranteed-dead pc + STUN/TURN
|
||||
/// gathering + answerer signaling in that case too.
|
||||
/// When force_relay *and* TURN are configured, WebRTC via TURN is a valid "relayed" path:
|
||||
/// Under relay-by-POLICY (force-always-relay option or an explicit relay request) the pc
|
||||
/// uses Relay-only ICE, which gathers nothing and can never connect unless a TURN server
|
||||
/// is configured, so skip building a guaranteed-dead pc + STUN/TURN gathering + answerer
|
||||
/// signaling in that case. WebSocket-forced relay is deliberately NOT policy: ws only
|
||||
/// tunnels the signaling/relay legs, so the offer keeps full ICE and may land a direct
|
||||
/// connection - the flagship path for ws deployments (`webrtc_all_ice` tells the peer).
|
||||
/// When policy relay *and* TURN are configured, WebRTC via TURN is a valid "relayed" path:
|
||||
/// `connect` keeps a WebRTC win instead of replacing it with the RustDesk relay, and the
|
||||
/// RelayResponse path races it without a P2P preference delay. Any request carrying an offer
|
||||
/// keeps its rendezvous socket for trickle signaling and never reuses it for TCP punching; a
|
||||
@@ -616,7 +622,7 @@ impl Client {
|
||||
if Config::is_proxy() {
|
||||
return false;
|
||||
}
|
||||
if interface.is_force_relay() && !WebRTCStream::has_turn_server() {
|
||||
if interface.is_policy_relay() && !WebRTCStream::has_turn_server() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@@ -883,6 +889,9 @@ impl Client {
|
||||
socket_addr_v6: ipv6.1.unwrap_or_default(),
|
||||
switch_code,
|
||||
webrtc_sdp_offer: webrtc_sdp_offer.clone(),
|
||||
// Full-ICE offer despite force_relay (ws transport): tells the controlled side
|
||||
// its answer may gather every candidate type instead of requiring TURN.
|
||||
webrtc_all_ice: !webrtc_sdp_offer.is_empty() && !interface.is_policy_relay(),
|
||||
..Default::default()
|
||||
});
|
||||
let webrtc_session_key = webrtc_offerer
|
||||
@@ -1064,10 +1073,12 @@ impl Client {
|
||||
Ok((Stream::WebRTC(raced), None, "WebRTC"))
|
||||
}
|
||||
.boxed();
|
||||
if interface.is_force_relay() {
|
||||
if interface.is_policy_relay() {
|
||||
// Relay-only WebRTC can use only TURN, so it has no P2P advantage
|
||||
// over the RustDesk relay. Take the first successful relay instead
|
||||
// of delaying an already-ready result for the preference window.
|
||||
// Policy, not force_relay: under ws the offer is full ICE and a
|
||||
// direct path is exactly what the preference window exists for.
|
||||
connect_futures.push(webrtc_fut);
|
||||
select_ok(connect_futures).await.map(|r| r.0)
|
||||
} else {
|
||||
@@ -1415,9 +1426,10 @@ impl Client {
|
||||
// disarmed only at the successful return when WebRTC is the kept transport.
|
||||
|
||||
let mut direct = !conn.is_err();
|
||||
// A WebRTC win under force_relay only happens when the pc was built with Relay-only ICE
|
||||
// (TURN configured), which already honors the relay requirement — keep it instead of
|
||||
// replacing it with the RustDesk relay.
|
||||
// Keep a WebRTC win instead of replacing it with the RustDesk relay: under relay-by-
|
||||
// policy the pc was built with Relay-only ICE (TURN configured), which already honors
|
||||
// the relay requirement, and under ws-forced relay a direct full-ICE connection is the
|
||||
// preferred outcome, not a violation.
|
||||
if (interface.is_force_relay() && typ != "WebRTC") || conn.is_err() {
|
||||
if !relay_server.is_empty() {
|
||||
let switch_code = interface.get_switch_code();
|
||||
@@ -2584,6 +2596,13 @@ pub struct LoginConfigHandler {
|
||||
// reconnect before the real reboot disconnect.
|
||||
restart_remote_device_at: Option<Instant>,
|
||||
pub force_relay: bool,
|
||||
// The policy component of force_relay: the user's relay choice (option or explicit
|
||||
// request) plus proxy, WITHOUT the WebSocket-transport component. ws kills classic
|
||||
// TCP/UDP punching (force_relay stays set for those paths) but says nothing about
|
||||
// ICE, so WebRTC decisions - offer ICE policy, prefer-P2P racing - key off this
|
||||
// instead: relay-by-policy must stay Relay-only ICE, relay-by-transport may go
|
||||
// direct over full ICE.
|
||||
pub policy_relay: bool,
|
||||
pub direct: Option<bool>,
|
||||
pub received: bool,
|
||||
switch_uuid: Option<String>,
|
||||
@@ -2696,11 +2715,11 @@ impl LoginConfigHandler {
|
||||
self.session_id = sid;
|
||||
self.supported_encoding = Default::default();
|
||||
self.clear_restarting_remote_device();
|
||||
self.force_relay =
|
||||
self.policy_relay =
|
||||
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
||||
|| force_relay
|
||||
|| use_ws()
|
||||
|| Config::is_proxy();
|
||||
self.force_relay = self.policy_relay || use_ws();
|
||||
if let Some((real_id, server, key)) = &self.other_server {
|
||||
let other_server_key = self.get_option("other-server-key");
|
||||
if !other_server_key.is_empty() && key.is_empty() {
|
||||
@@ -4592,6 +4611,10 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
||||
self.get_lch().read().unwrap().force_relay
|
||||
}
|
||||
|
||||
fn is_policy_relay(&self) -> bool {
|
||||
self.get_lch().read().unwrap().policy_relay
|
||||
}
|
||||
|
||||
fn get_switch_code(&self) -> String {
|
||||
match self.get_lch().read().unwrap().switch_uuid.clone() {
|
||||
Some(u) if !u.is_empty() => {
|
||||
|
||||
@@ -689,13 +689,13 @@ impl RendezvousMediator {
|
||||
async fn spawn_webrtc_answerer(
|
||||
&self,
|
||||
ph: &PunchHole,
|
||||
force_relay: bool,
|
||||
relay_only_ice: bool,
|
||||
server: ServerPtr,
|
||||
peer_addr: SocketAddr,
|
||||
meta: ConnectionMeta,
|
||||
) -> ResultType<String> {
|
||||
let mut stream =
|
||||
WebRTCStream::new(&ph.webrtc_sdp_offer, force_relay, CONNECT_TIMEOUT).await?;
|
||||
WebRTCStream::new(&ph.webrtc_sdp_offer, relay_only_ice, CONNECT_TIMEOUT).await?;
|
||||
let answer = match stream.get_local_endpoint_trickle().await {
|
||||
Ok(answer) => answer,
|
||||
Err(e) => {
|
||||
@@ -849,14 +849,18 @@ impl RendezvousMediator {
|
||||
// and STUN bypass the proxy and leak the real IP. WebSocket mode does NOT disable it —
|
||||
// ws only tunnels the signaling/relay legs to the server, classic punching stays forced
|
||||
// to relay (`relay` above), and the answer rides the RelayResponse, leaving ICE as the
|
||||
// only P2P path there. force_relay is different: relay-only ICE is viable with TURN.
|
||||
// only P2P path there. force_relay depends on why it was set: with webrtc_all_ice the
|
||||
// controller's relay is transport-forced (ws) and its offer carries every candidate
|
||||
// type, so answer with full ICE and let a direct pair form; without it the offer is
|
||||
// Relay-only ICE by policy, viable (and answerable) only through TURN.
|
||||
let webrtc_relay_only = ph.force_relay && !ph.webrtc_all_ice;
|
||||
let webrtc_viable = !ph.webrtc_sdp_offer.is_empty()
|
||||
&& !Config::is_proxy()
|
||||
&& (!ph.force_relay || WebRTCStream::has_turn_server());
|
||||
&& (!webrtc_relay_only || WebRTCStream::has_turn_server());
|
||||
let webrtc_sdp_answer = if webrtc_viable {
|
||||
self.spawn_webrtc_answerer(
|
||||
&ph,
|
||||
ph.force_relay,
|
||||
webrtc_relay_only,
|
||||
server.clone(),
|
||||
peer_addr,
|
||||
meta.clone(),
|
||||
|
||||
@@ -1294,7 +1294,11 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
|
||||
// override only if true
|
||||
if true == force_relay {
|
||||
self.lc.write().unwrap().force_relay = true;
|
||||
let mut lc = self.lc.write().unwrap();
|
||||
lc.force_relay = true;
|
||||
// An explicit retry-via-relay is user policy, not transport necessity: keep
|
||||
// WebRTC on Relay-only ICE for this round like any force-always-relay session.
|
||||
lc.policy_relay = true;
|
||||
}
|
||||
self.lc.write().unwrap().peer_info = None;
|
||||
self.reconnect_count.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
Reference in New Issue
Block a user