From 00f0a720b5466d5e78d4c92173b72298e44373d2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 2 Sep 2026 21:48:19 +0800 Subject: [PATCH] webrtc: decode TURN userinfo before handing it to the ICE agent `Url` returns the username and password percent-encoded, and they went into the ICE server configuration as-is, so a password containing `@`, `/`, `?`, `#`, `%` or a space (written encoded, the only way the URL can carry it) failed the long-term credential check with a 401 and TURN never produced a relay candidate. Decode both, as the web client does. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JXJJGEGdgu26wgCppvUXdZ --- Cargo.toml | 1 + src/webrtc.rs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 09d285095..6b6f62615 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/webrtc.rs b/src/webrtc.rs index 57c609e8d..e0189f4d7 100644 --- a/src/webrtc.rs +++ b/src/webrtc.rs @@ -41,6 +41,7 @@ use webrtc::peer_connection::RTCPeerConnection; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::Engine; use bytes::{Buf, BufMut, Bytes, BytesMut}; +use percent_encoding::percent_decode_str; use tokio::sync::{mpsc, watch, Mutex, Semaphore}; use tokio::time::{timeout, timeout_at, Instant}; use url::Url; @@ -395,8 +396,14 @@ impl WebRTCStream { if host.is_empty() { return None; } - let username = u.username().to_string(); - let credential = u.password().unwrap_or_default().to_string(); + // `Url` hands userinfo back percent-encoded; the TURN server expects the value, + // so `p%40ss` must reach it as `p@ss` for the long-term credential to verify. + let username = percent_decode_str(u.username()) + .decode_utf8_lossy() + .into_owned(); + let credential = percent_decode_str(u.password().unwrap_or_default()) + .decode_utf8_lossy() + .into_owned(); // A TURN server without credentials is not merely useless: webrtc-rs validates every // configured server in `new_peer_connection`, so one credential-less entry makes EVERY // peer connection fail — including plain non-relay ones that never wanted TURN. The @@ -1468,6 +1475,14 @@ mod tests { "turn:example.com:3478" ); + // Userinfo arrives percent-encoded from the URL parser; the TURN server wants the + // value, so a password written `p%40ss` authenticates as `p@ss`. + let encoded = + WebRTCStream::get_ice_server_from_url("turn://us%20er:p%40s%2Fs%25@example.com") + .expect("encoded credentials are usable"); + assert_eq!(encoded.username, "us er"); + assert_eq!(encoded.credential, "p@s/s%"); + // TURN without both halves of the credential is dropped rather than passed on: // webrtc-rs validates every configured server when the peer connection is built, so // one such entry fails EVERY connection, including those that never wanted TURN.