Merge pull request #579 from rustdesk/webrtc

Webrtc
This commit is contained in:
RustDesk
2026-09-06 00:22:34 +08:00
committed by GitHub
10 changed files with 2377 additions and 264 deletions
+7 -2
View File
@@ -52,6 +52,7 @@ thiserror = "1.0"
httparse = "1.10" httparse = "1.10"
base64 = "0.22" base64 = "0.22"
url = "2.5" url = "2.5"
percent-encoding = "2"
sha2 = "0.10" sha2 = "0.10"
whoami = "1.5" whoami = "1.5"
@@ -68,7 +69,11 @@ rustls-pki-types = "1.11"
rustls-native-certs = "0.8" rustls-native-certs = "0.8"
webpki-roots = "1.0.4" webpki-roots = "1.0.4"
async-recursion = "1.1" 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" libloading = "0.8"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
@@ -81,7 +86,7 @@ protobuf-codegen = { version = "3.7" }
[dev-dependencies] [dev-dependencies]
clap = "4.5.51" 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] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = [ winapi = { version = "0.3", features = [
+80
View File
@@ -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
View File
@@ -38,6 +38,10 @@ message VideoFrame {
message IdPk { message IdPk {
string id = 1; string id = 1;
bytes pk = 2; 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 { message DisplayInfo {
+16
View File
@@ -29,6 +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;
} }
message ControlPermissions { message ControlPermissions {
@@ -64,6 +68,7 @@ message PunchHole {
bytes socket_addr_v6 = 7; bytes socket_addr_v6 = 7;
ControlPermissions control_permissions = 8; ControlPermissions control_permissions = 8;
ControlledContext controlled_context = 9; ControlledContext controlled_context = 9;
string webrtc_sdp_offer = 10;
} }
message TestNatRequest { message TestNatRequest {
@@ -90,6 +95,7 @@ message PunchHoleSent {
string version = 5; string version = 5;
int32 upnp_port = 6; int32 upnp_port = 6;
bytes socket_addr_v6 = 7; bytes socket_addr_v6 = 7;
string webrtc_sdp_answer = 8;
} }
message RegisterPk { message RegisterPk {
@@ -135,6 +141,7 @@ message PunchHoleResponse {
bool is_udp = 9; bool is_udp = 9;
int32 upnp_port = 10; int32 upnp_port = 10;
bytes socket_addr_v6 = 11; bytes socket_addr_v6 = 11;
string webrtc_sdp_answer = 12;
} }
message ConfigUpdate { message ConfigUpdate {
@@ -169,6 +176,7 @@ message RelayResponse {
int32 feedback = 9; int32 feedback = 9;
bytes socket_addr_v6 = 10; bytes socket_addr_v6 = 10;
int32 upnp_port = 11; int32 upnp_port = 11;
string webrtc_sdp_answer = 12;
} }
message SoftwareUpdate { string url = 1; } message SoftwareUpdate { string url = 1; }
@@ -240,6 +248,13 @@ message HttpProxyResponse {
string error = 4; string error = 4;
} }
message IceCandidate {
string id = 1;
bytes socket_addr = 2;
string session_key = 3;
string candidate = 4;
}
message RendezvousMessage { message RendezvousMessage {
oneof union { oneof union {
RegisterPeer register_peer = 6; RegisterPeer register_peer = 6;
@@ -265,5 +280,6 @@ message RendezvousMessage {
HealthCheck hc = 26; HealthCheck hc = 26;
HttpProxyRequest http_proxy_request = 27; HttpProxyRequest http_proxy_request = 27;
HttpProxyResponse http_proxy_response = 28; HttpProxyResponse http_proxy_response = 28;
IceCandidate ice_candidate = 29;
} }
} }
+4 -2
View File
@@ -4,6 +4,8 @@ use tokio_util::codec::{Decoder, Encoder};
// Bound speculative allocation from untrusted frame headers. // Bound speculative allocation from untrusted frame headers.
const MAX_PREALLOCATED_PAYLOAD_LEN: usize = 256 * 1024; 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)] #[derive(Debug, Clone, Copy)]
pub struct BytesCodec { pub struct BytesCodec {
@@ -132,7 +134,7 @@ impl Encoder<Bytes> for BytesCodec {
let h = (data.len() << 2) as u32 | 0x2; let h = (data.len() << 2) as u32 | 0x2;
buf.put_u16_le((h & 0xFFFF) as u16); buf.put_u16_le((h & 0xFFFF) as u16);
buf.put_u8((h >> 16) as u8); 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); buf.put_u32_le((data.len() << 2) as u32 | 0x3);
} else { } else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow")); return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
@@ -290,7 +292,7 @@ mod tests {
fn decode_large_frame_header_caps_preallocation() { fn decode_large_frame_header_caps_preallocation() {
let mut codec = BytesCodec::new(); let mut codec = BytesCodec::new();
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();
let n = 0x3FFFFFFFusize; let n = MAX_FRAME_LENGTH;
const MAX_REASONABLE_CAPACITY: usize = MAX_PREALLOCATED_PAYLOAD_LEN * 4; const MAX_REASONABLE_CAPACITY: usize = MAX_PREALLOCATED_PAYLOAD_LEN * 4;
buf.put_u32_le((n << 2) as u32 | 0x3); buf.put_u32_le((n << 2) as u32 | 0x3);
+8
View File
@@ -2994,9 +2994,13 @@ pub mod keys {
"allow-command-line-settings-when-settings-disabled"; "allow-command-line-settings-when-settings-disabled";
// Connection punch-through / port-forward options // 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_UDP_PUNCH: &str = "enable-udp-punch";
pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-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_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_USERNAME_ON_CARD: &str = "hide-username-on-card";
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
@@ -3133,9 +3137,11 @@ pub mod keys {
OPTION_ALLOW_AUTO_RECORD_OUTGOING, OPTION_ALLOW_AUTO_RECORD_OUTGOING,
OPTION_HIDE_RECORDING_BUTTON, OPTION_HIDE_RECORDING_BUTTON,
OPTION_VIDEO_SAVE_DIRECTORY, OPTION_VIDEO_SAVE_DIRECTORY,
OPTION_ENABLE_TCP_PUNCH,
OPTION_ENABLE_UDP_PUNCH, OPTION_ENABLE_UDP_PUNCH,
OPTION_ENABLE_IPV6_PUNCH, OPTION_ENABLE_IPV6_PUNCH,
OPTION_ENABLE_PORT_FORWARD_MUX, OPTION_ENABLE_PORT_FORWARD_MUX,
OPTION_ENABLE_WEBRTC,
OPTION_TOUCH_MODE, OPTION_TOUCH_MODE,
OPTION_SHOW_VIRTUAL_MOUSE, OPTION_SHOW_VIRTUAL_MOUSE,
OPTION_SHOW_VIRTUAL_JOYSTICK, OPTION_SHOW_VIRTUAL_JOYSTICK,
@@ -3202,6 +3208,8 @@ pub mod keys {
OPTION_ALLOW_INSECURE_TLS_FALLBACK, OPTION_ALLOW_INSECURE_TLS_FALLBACK,
OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS,
OPTION_ALLOW_AUTO_UPDATE, OPTION_ALLOW_AUTO_UPDATE,
OPTION_ALLOW_KCP_CC,
OPTION_ALLOW_WEBRTC_CC,
]; ];
// BUILDIN_SETTINGS // BUILDIN_SETTINGS
+19 -3
View File
@@ -57,6 +57,7 @@ pub use toml;
pub use uuid; pub use uuid;
pub mod fingerprint; pub mod fingerprint;
pub use flexi_logger; pub use flexi_logger;
pub mod log_throttle;
pub mod stream; pub mod stream;
pub mod websocket; pub mod websocket;
#[cfg(feature = "webrtc")] #[cfg(feature = "webrtc")]
@@ -429,10 +430,15 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
#[allow(unused_mut)] #[allow(unused_mut)]
let mut logger_holder: Option<flexi_logger::LoggerHandle> = None; let mut logger_holder: Option<flexi_logger::LoggerHandle> = None;
INIT.call_once(|| { 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)] #[cfg(debug_assertions)]
{ {
use env_logger::*; 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))] #[cfg(not(debug_assertions))]
{ {
@@ -447,7 +453,7 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
path.push(_name); path.push(_name);
} }
use flexi_logger::*; 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 logger_holder = x
.log_to_file(FileSpec::default().directory(path)) .log_to_file(FileSpec::default().directory(path))
.write_mode(if _is_async { .write_mode(if _is_async {
@@ -457,8 +463,18 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
}) })
.format(opt_format) .format(opt_format)
.rotate( .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, 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), Cleanup::KeepLogFiles(90),
) )
.start() .start()
+154
View File
@@ -0,0 +1,154 @@
use std::sync::Mutex;
use std::time::{Duration, Instant};
/// Collapses a log site whose call rate is set by someone else — a peer's message rate, or a
/// retry loop — into at most one line per interval.
///
/// Debug output is written to the log file, so a site that fires per received packet lets a
/// peer decide how much a machine writes to disk. Dropping the line entirely instead would
/// hide real faults, so keep one line per interval and carry the count of everything
/// suppressed since the last one.
///
/// Prefer the [`throttled_log!`](crate::throttled_log) macro, which declares the static for
/// you. Reach for this type directly only when the count belongs somewhere other than the end
/// of the line, or when the decision drives more than a log call.
///
/// Declare one per site (they do not share counts):
///
/// ```ignore
/// static DROPPED_ICE: LogThrottle = LogThrottle::new(Duration::from_secs(60));
///
/// if let Some(n) = DROPPED_ICE.due() {
/// log::debug!("dropped {n} ICE candidate(s) with no route");
/// }
/// ```
pub struct LogThrottle {
interval: Duration,
state: Mutex<ThrottleState>,
}
struct ThrottleState {
suppressed: u64,
last: Option<Instant>,
}
impl LogThrottle {
pub const fn new(interval: Duration) -> Self {
Self {
interval,
state: Mutex::new(ThrottleState {
suppressed: 0,
last: None,
}),
}
}
/// Record one occurrence. Returns the number of occurrences to report (including this one)
/// when a line is due, or `None` while still inside the interval.
///
/// The first occurrence after a quiet period always reports, so an isolated fault is not
/// delayed by the interval.
pub fn due(&self) -> Option<u64> {
// A poisoned lock only means some other thread panicked while holding it; the guarded
// data is two plain counters that are still usable, and going silent for the rest of
// the process would be worse than a stale count.
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
state.suppressed += 1;
// `map_or(true, ..)` rather than clippy's preferred `is_none_or`: that was stabilized in
// Rust 1.82 and this crate builds on the 1.75 pinned by CI.
#[allow(clippy::unnecessary_map_or)]
let due = state
.last
.map_or(true, |last| last.elapsed() >= self.interval);
if !due {
return None;
}
state.last = Some(Instant::now());
Some(std::mem::replace(&mut state.suppressed, 0))
}
}
/// Log at most one line per interval from this call site, suffixed with the number of
/// occurrences it stands for.
///
/// Each expansion declares its own hidden static, so two sites never share a count and
/// adding one is a single line:
///
/// ```ignore
/// throttled_log!(Duration::from_secs(5), warn, "rejected ipc peer {peer_pid:?}");
/// ```
///
/// An isolated event logs unchanged; a burst collapses to `... (x47)`. The count includes
/// the occurrence being reported, so it reads as a total rather than as "and N more".
#[macro_export]
macro_rules! throttled_log {
($interval:expr, $level:ident, $($arg:tt)+) => {{
static THROTTLE: $crate::log_throttle::LogThrottle =
$crate::log_throttle::LogThrottle::new($interval);
if let Some(n) = THROTTLE.due() {
if n > 1 {
$crate::log::$level!("{} (x{})", format_args!($($arg)+), n);
} else {
$crate::log::$level!("{}", format_args!($($arg)+));
}
}
}};
}
#[cfg(test)]
mod tests {
use super::*;
// Two directions of one socket need two throttles: an ICMP error on a connected socket is
// reported once and cleared, so the steady state alternates (send succeeds, the next recv
// reports it) and anything shared between them is reset by the succeeding side every cycle.
#[test]
fn separate_throttles_do_not_reset_each_other() {
let send = LogThrottle::new(Duration::from_secs(60));
let recv = LogThrottle::new(Duration::from_secs(60));
assert_eq!(recv.due(), Some(1));
for _ in 0..1_000 {
// The send side succeeding must not hand the recv side a fresh emit slot.
assert_eq!(recv.due(), None);
}
assert_eq!(
send.due(),
Some(1),
"the other direction keeps its own slot"
);
}
#[test]
fn first_call_reports_immediately() {
let t = LogThrottle::new(Duration::from_secs(60));
assert_eq!(t.due(), Some(1));
}
#[test]
fn calls_inside_the_interval_are_counted_not_reported() {
let t = LogThrottle::new(Duration::from_secs(60));
assert_eq!(t.due(), Some(1));
for _ in 0..100 {
assert_eq!(t.due(), None);
}
}
#[test]
fn the_next_due_line_carries_everything_suppressed() {
let t = LogThrottle::new(Duration::ZERO);
assert_eq!(t.due(), Some(1));
// A zero interval is always due, so each call reports exactly itself.
assert_eq!(t.due(), Some(1));
let t = LogThrottle::new(Duration::from_millis(30));
assert_eq!(t.due(), Some(1));
assert_eq!(t.due(), None);
assert_eq!(t.due(), None);
std::thread::sleep(Duration::from_millis(40));
// The two suppressed calls plus this one.
assert_eq!(t.due(), Some(3));
}
}
+96 -7
View File
@@ -74,6 +74,87 @@ impl Stream {
} }
} }
/// Whether this is a WebRTC transport. Used to enforce the WebRTC-only DTLS fingerprint
/// binding (fail closed) in the secure handshake.
#[inline]
pub fn is_webrtc(&self) -> bool {
match self {
#[cfg(feature = "webrtc")]
Stream::WebRTC(_) => true,
#[allow(unreachable_patterns)]
_ => false,
}
}
/// Close the underlying WebRTC peer connection if this is a WebRTC stream; no-op otherwise.
/// A WebRTC pc is kept alive by the global session cache and its cleanup handler only fires on
/// a terminal ICE state, so it must be closed explicitly at session end. TCP/WebSocket streams
/// release their resources on drop and need nothing here.
///
/// Deliberately not `async`: most callers sit in a `select!` arm or a future the UI can
/// abandon, and an awaited close that loses that race is unretryable — `close()` latches
/// `is_closed` before its first await, so every later attempt early-returns while the state
/// handler that would evict the session never runs. With no await point here there is
/// nothing to cancel; the teardown runs to completion on the WebRTC I/O runtime
/// (see `webrtc::WEBRTC_RT`) that owns the pc's sockets and pump tasks, so it reaches the
/// wire even after a caller's own runtime dies on return.
#[inline]
pub fn close_webrtc(&self) {
match self {
#[cfg(feature = "webrtc")]
Stream::WebRTC(s) => s.close_detached(),
#[allow(unreachable_patterns)]
_ => {}
}
}
/// Whether an established WebRTC transport runs through a TURN relay (used for the UI's
/// direct/relayed flag). `None` for non-WebRTC transports, and for a non-relay-policy pc
/// before ICE selects a pair; a Relay-policy pc answers `Some(true)` straight away — see
/// `WebRTCStream::is_relayed`.
#[inline]
pub async fn webrtc_relayed(&self) -> Option<bool> {
match self {
#[cfg(feature = "webrtc")]
Stream::WebRTC(s) => s.is_relayed().await,
#[allow(unreachable_patterns)]
_ => None,
}
}
/// Whether an established WebRTC transport reaches the peer over IPv6 (used to name the
/// transport in the UI). `None` for non-WebRTC transports and before ICE selects a pair —
/// every other transport already carries the family in the label it was raced under.
#[inline]
pub async fn webrtc_remote_ipv6(&self) -> Option<bool> {
match self {
#[cfg(feature = "webrtc")]
Stream::WebRTC(s) => s.is_remote_ipv6().await,
#[allow(unreachable_patterns)]
_ => None,
}
}
/// DTLS certificate fingerprint for a WebRTC stream (`local`=true for this endpoint's own
/// cert, false for the peer's), used to bind the channel to the signed peer identity.
/// Returns None for non-WebRTC transports, which authenticate via the secretbox key exchange.
#[inline]
#[cfg_attr(not(feature = "webrtc"), allow(unused_variables))]
pub async fn dtls_fingerprint(&self, local: bool) -> Option<String> {
match self {
#[cfg(feature = "webrtc")]
Stream::WebRTC(s) => {
if local {
s.local_dtls_fingerprint().await.ok()
} else {
s.remote_dtls_fingerprint().await.ok()
}
}
#[allow(unreachable_patterns)]
_ => None,
}
}
#[inline] #[inline]
pub async fn next_timeout( pub async fn next_timeout(
&mut self, &mut self,
@@ -137,13 +218,21 @@ impl Stream {
pub fn from(stream: TcpStream, stream_addr: SocketAddr) -> Self { pub fn from(stream: TcpStream, stream_addr: SocketAddr) -> Self {
Self::Tcp(tcp::FramedStream::from(stream, stream_addr)) Self::Tcp(tcp::FramedStream::from(stream, stream_addr))
} }
}
#[inline] /// Owning the stream owns the transport, WebRTC included.
#[cfg(feature = "webrtc")] ///
pub fn get_webrtc_stream(&self) -> Option<webrtc::WebRTCStream> { /// A peer connection outlives its handle — the session cache holds a clone, and the handler that
match self { /// evicts it only fires on a terminal ICE state — so it has to be closed explicitly. Doing that
Self::WebRTC(s) => Some(s.clone()), /// at each exit path made it an obligation every `return`, `break` and `?` had to remember, and
_ => None, /// the long-lived side never did: `server::connection` ends its ~15 exits by dropping the stream.
} /// Nothing warned, because a missed close leaks silently and only under WebRTC.
///
/// `Stream` is not `Clone`, so dropping it really is the end of the transport and there is no
/// second owner to surprise. Explicit `close_webrtc()` calls remain valid — they close sooner
/// than scope end — but they are now an optimization rather than the thing correctness rests on.
impl Drop for Stream {
fn drop(&mut self) {
self.close_webrtc();
} }
} }
+1970 -231
View File
File diff suppressed because it is too large Load Diff