From ab9c529f9fa0052e65a5994155489466e320ed37 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 15:59:48 +1100 Subject: [PATCH 01/10] refactor: split out `str0m-ice` crate --- Cargo.lock | 13 + Cargo.toml | 1 + ice/Cargo.toml | 28 + {src/ice => ice/src}/agent.rs | 16 +- {src/ice => ice/src}/candidate.rs | 14 +- {src/io => ice/src}/error.rs | 17 + {src/io => ice/src}/id.rs | 0 ice/src/io.rs | 371 ++++++++ ice/src/lib.rs | 60 ++ {src/ice => ice/src}/pair.rs | 8 +- {src/ice => ice/src}/preference.rs | 0 ice/src/sdp.rs | 258 +++++ {src/io => ice/src}/stun.rs | 38 +- ice/src/util.rs | 65 ++ ice/tests/lib.rs | 251 +++++ src/_internal_test_exports/mod.rs | 2 +- src/error.rs | 2 +- src/ice/error.rs | 19 - src/ice/mod.rs | 1422 ---------------------------- src/io/mod.rs | 252 +---- src/lib.rs | 23 +- src/sdp/mod.rs | 1 - src/sdp/parser.rs | 1 + src/util/mod.rs | 1 + 24 files changed, 1172 insertions(+), 1691 deletions(-) create mode 100644 ice/Cargo.toml rename {src/ice => ice/src}/agent.rs (99%) rename {src/ice => ice/src}/candidate.rs (99%) rename {src/io => ice/src}/error.rs (82%) rename {src/io => ice/src}/id.rs (100%) create mode 100644 ice/src/io.rs create mode 100644 ice/src/lib.rs rename {src/ice => ice/src}/pair.rs (99%) rename {src/ice => ice/src}/preference.rs (100%) create mode 100644 ice/src/sdp.rs rename {src/io => ice/src}/stun.rs (98%) create mode 100644 ice/src/util.rs create mode 100644 ice/tests/lib.rs delete mode 100644 src/ice/error.rs delete mode 100644 src/ice/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 344a58fb0..b44bd6ca7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,6 +1786,7 @@ dependencies = [ "serde_json", "str0m-apple-crypto", "str0m-aws-lc-rs", + "str0m-ice", "str0m-netem", "str0m-openssl", "str0m-proto", @@ -1823,6 +1824,18 @@ dependencies = [ "str0m-proto", ] +[[package]] +name = "str0m-ice" +version = "0.1.0" +dependencies = [ + "crc", + "fastrand", + "serde", + "str0m-proto", + "subtle", + "tracing", +] + [[package]] name = "str0m-netem" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index d4d2042dd..d556187b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ serde = { version = "1.0.152", features = ["derive"] } dimpl = { version = "0.2.4", default-features = false } str0m-proto = { version = "0.1.2", path = "proto" } +str0m-ice = { version = "0.1.0", path = "ice" } # Crypto providers str0m-aws-lc-rs = { version = "0.1.2", path = "crypto/aws-lc-rs", optional = true } diff --git a/ice/Cargo.toml b/ice/Cargo.toml new file mode 100644 index 000000000..4a9742010 --- /dev/null +++ b/ice/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "str0m-ice" +version = "0.1.0" +authors = ["Martin Algesten ", "Hugo Tunius ", "Davide Bertola "] +description = "ICE (Interactive Connectivity Establishment) implementation for str0m" +license = "MIT OR Apache-2.0" +repository = "https://github.com/algesten/str0m" +readme = "README.md" +edition = "2021" + +# MSRV +rust-version = "1.81.0" + +[features] +default = [] +_internal_test_exports = [] +# Redacts personally identifiable information (PII) from logs debug and above +pii = [] + +[dependencies] +tracing = "0.1.37" +fastrand = "2.0.1" +subtle = "2.0.0" +serde = { version = "1.0.152", features = ["derive"] } +crc = ">=3.0, <3.4" +str0m-proto = { version = "0.1.2", path = "../proto" } + +[dev-dependencies] diff --git a/src/ice/agent.rs b/ice/src/agent.rs similarity index 99% rename from src/ice/agent.rs rename to ice/src/agent.rs index 616b5e0ca..bc201a3be 100644 --- a/src/ice/agent.rs +++ b/ice/src/agent.rs @@ -6,17 +6,21 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; +use tracing::{debug, trace, warn}; -use crate::crypto::Sha1HmacProvider; -use crate::ice_::preference::default_local_preference; -use crate::io::{Id, StunClass, StunMethod, StunTiming, DATAGRAM_MTU_WARN}; +use crate::io::Transmit; use crate::io::{Protocol, StunPacket}; use crate::io::{StunMessage, TransId}; -use crate::io::{Transmit, DATAGRAM_MTU}; +use crate::preference::default_local_preference; +use crate::stun::{Id, StunTiming}; use crate::util::{NonCryptographicRng, Pii}; +use crate::{StunClass, StunMethod}; -use super::candidate::{Candidate, CandidateKind}; -use super::pair::{CandidatePair, CheckState, PairId}; +use str0m_proto::crypto::Sha1HmacProvider; +use str0m_proto::{DATAGRAM_MTU, DATAGRAM_MTU_WARN}; + +use crate::candidate::{Candidate, CandidateKind}; +use crate::pair::{CandidatePair, CheckState, PairId}; /// Handles the ICE protocol for a given peer. /// diff --git a/src/ice/candidate.rs b/ice/src/candidate.rs similarity index 99% rename from src/ice/candidate.rs rename to ice/src/candidate.rs index b3fe78d7b..3b04af214 100644 --- a/src/ice/candidate.rs +++ b/ice/src/candidate.rs @@ -1,4 +1,4 @@ -use super::IceError; +use crate::error::IceError; use crate::io::{Protocol, TcpType}; use crate::sdp::parse_candidate; use serde::ser::SerializeStruct; @@ -195,7 +195,7 @@ impl Candidate { } #[allow(clippy::too_many_arguments)] - pub(crate) fn parsed( + pub fn parsed( foundation: String, component_id: u16, proto: Protocol, @@ -370,7 +370,6 @@ impl Candidate { } } - #[cfg(test)] pub(crate) fn test_peer_rflx( addr: SocketAddr, base: SocketAddr, @@ -533,15 +532,18 @@ impl Candidate { self.discarded } - pub(crate) fn set_ufrag(&mut self, ufrag: &str) { + #[doc(hidden)] // Private API. + pub fn set_ufrag(&mut self, ufrag: &str) { self.ufrag = Some(ufrag.into()); } - pub(crate) fn ufrag(&self) -> Option<&str> { + #[doc(hidden)] // Private API. + pub fn ufrag(&self) -> Option<&str> { self.ufrag.as_deref() } - pub(crate) fn clear_ufrag(&mut self) { + #[doc(hidden)] // Private API. + pub fn clear_ufrag(&mut self) { self.ufrag = None; } diff --git a/src/io/error.rs b/ice/src/error.rs similarity index 82% rename from src/io/error.rs rename to ice/src/error.rs index f6e04dae3..c41681a89 100644 --- a/src/io/error.rs +++ b/ice/src/error.rs @@ -2,6 +2,23 @@ use std::error::Error; use std::fmt; use std::io; +/// Errors from the ICE agent. +#[allow(missing_docs)] +#[derive(Debug)] +pub enum IceError { + BadCandidate(String), +} + +impl fmt::Display for IceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IceError::BadCandidate(msg) => write!(f, "ICE bad candidate: {}", msg), + } + } +} + +impl Error for IceError {} + /// A STUN message could not be parsed or processed. #[derive(Debug)] pub enum StunError { diff --git a/src/io/id.rs b/ice/src/id.rs similarity index 100% rename from src/io/id.rs rename to ice/src/id.rs diff --git a/ice/src/io.rs b/ice/src/io.rs new file mode 100644 index 000000000..f3533dbce --- /dev/null +++ b/ice/src/io.rs @@ -0,0 +1,371 @@ +//! Network I/O types and STUN protocol implementation. + +use std::convert::TryFrom; +use std::fmt; +use std::io; +use std::net::SocketAddr; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use crate::NetError; +// Re-export from our internal modules +pub use crate::stun::{StunMessage, StunMessageBuilder, TransId}; + +/// Max UDP packet size +#[allow(dead_code)] +pub(crate) const DATAGRAM_MAX_PACKET_SIZE: usize = 2000; + +/// Max expected RTP header over, with full extensions etc. +#[allow(dead_code)] +pub const MAX_RTP_OVERHEAD: usize = 80; + +/// Type of protocol used in [`Transmit`] and [`Receive`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Protocol { + /// UDP + Udp, + /// TCP (See RFC 4571 for framing) + Tcp, + /// TCP with fixed SSL Hello Exchange + /// See AsyncSSLServerSocket implementation for exchange details: + /// + SslTcp, + /// TLS (only used via relay) + Tls, +} + +/// TCP connection role as defined by the `tcptype` SDP attribute. +/// +/// This enum corresponds to the TCP connection setup modes defined in +/// [RFC 6544 §4.5](https://datatracker.ietf.org/doc/html/rfc6544#section-4.5), +/// which specifies how endpoints establish TCP connections when TCP is used +/// as a transport for media streams. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TcpType { + /// The endpoint actively initiates the TCP connection. + /// + /// In this mode, the endpoint performs an active open (i.e., sends a SYN) + /// to the remote peer. + Active, + + /// The endpoint passively waits for an incoming TCP connection. + /// + /// In this mode, the endpoint performs a passive open (i.e., listens) + /// and accepts a connection initiated by the remote peer. + Passive, + + /// Simultaneous open. + /// + /// Both endpoints attempt to actively open a TCP connection to each other + /// at the same time. This relies on TCP simultaneous open behavior. + So, +} + +impl Protocol { + /// Returns the protocol as a string slice. + pub fn as_str(&self) -> &'static str { + match self { + Protocol::Udp => "udp", + Protocol::Tcp => "tcp", + Protocol::SslTcp => "ssltcp", + Protocol::Tls => "tls", + } + } +} + +impl fmt::Display for TcpType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + Self::Active => "active", + Self::Passive => "passive", + Self::So => "so", + }; + f.write_str(str) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseTcpTypeError; + +impl fmt::Display for ParseTcpTypeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid TCP type (expected: active, passive, or so)") + } +} + +impl std::error::Error for ParseTcpTypeError {} + +impl FromStr for TcpType { + type Err = ParseTcpTypeError; + + fn from_str(s: &str) -> Result { + match s { + _ if s.eq_ignore_ascii_case("active") => Ok(Self::Active), + _ if s.eq_ignore_ascii_case("passive") => Ok(Self::Passive), + _ if s.eq_ignore_ascii_case("so") => Ok(Self::So), + _ => Err(ParseTcpTypeError), + } + } +} + +/// An instruction to send an outgoing packet. +#[derive(Serialize, Deserialize)] +pub struct Transmit { + /// Protocol the transmission should use. + /// + /// Provided to each of the [`Candidate`][crate::Candidate] constructors. + pub proto: Protocol, + + /// The source IP this packet should be sent from. + /// + /// For ICE it's important to send outgoing packets from the correct IP address. + /// The IP could come from a local socket or relayed over a TURN server. Features like + /// hole-punching will only work if the packets are routed through the correct interfaces. + /// + /// This address will either be: + /// - The address of a socket you have bound locally, such as + /// with [`UdpSocket::bind`][std::net::UdpSocket]. + /// - The address of a relay socket that you have + /// [allocated](https://www.rfc-editor.org/rfc/rfc8656#name-allocations-2) using TURN. + /// + /// To correctly handle an instance of [`Transmit`], you should: + /// + /// - Check if [`Transmit::source`] corresponds to one of your local sockets, + /// if yes, send it through that. + /// - Check if [`Transmit::source`] corresponds to one of your relay sockets (i.e. allocations), + /// if yes, send it via one of: + /// - a [TURN channel data message](https://www.rfc-editor.org/rfc/rfc8656#name-sending-a-channeldata-messa) + /// - a [SEND indication](https://www.rfc-editor.org/rfc/rfc8656#name-send-and-data-methods) + /// + /// `str0m` learns about the source address using [`Candidate`][crate::Candidate] that are added using + /// [`Rtc::add_local_candidate`][crate::Rtc::add_local_candidate]. + /// + /// The different candidate types are: + /// + /// * [`Candidate::host()`][crate::Candidate::host]: Used for locally bound UDP sockets. + /// * [`Candidate::relayed()`][crate::Candidate::relayed]: Used for sockets relayed via some + /// other server (normally TURN). + /// * [`Candidate::server_reflexive()`][crate::Candidate::server_reflexive]: Used when a local + /// (host) socket appears as some another IP address to the remote peer (usually due to a + /// NAT firewall on the local side). STUN servers can be used to discover the external address. + /// In this case the `base` parameter to `server_reflexive()` is the local address and + /// used for [`Transmit::source`]. + /// * `Peer reflexive` is another, internal, type of candidate that str0m infers by using the other + /// types of candidates. + pub source: SocketAddr, + + /// The destination address this datagram should be sent to. + /// + /// This will be one of the [`Candidate`][crate::Candidate] provided explicitly using + /// [`Rtc::add_remote_candidate`][crate::Rtc::add_remote_candidate] or via SDP negotiation. + pub destination: SocketAddr, + + /// Contents of the datagram. + pub contents: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +/// Received incoming data. +pub struct Receive<'a> { + /// The protocol the socket this received data originated from is using. + pub proto: Protocol, + + /// The socket this received data originated from. + pub source: SocketAddr, + + /// The destination ip of the datagram. + pub destination: SocketAddr, + + /// Parsed contents of the datagram. + #[serde(borrow)] + pub contents: DatagramRecv<'a>, +} + +impl<'a> Receive<'a> { + /// Creates a new instance by trying to parse the contents of `buf`. + pub fn new( + proto: Protocol, + source: SocketAddr, + destination: SocketAddr, + buf: &'a [u8], + ) -> Result { + let contents = DatagramRecv::try_from(buf)?; + Ok(Receive { + proto, + source, + destination, + contents, + }) + } +} + +/// An incoming STUN packet. +#[derive(Debug)] +pub struct StunPacket<'a> { + /// The protocol the socket this received data originated from is using. + pub proto: Protocol, + /// The socket this received data originated from. + pub source: SocketAddr, + /// The destination socket of the datagram. + pub destination: SocketAddr, + /// The STUN message. + pub message: StunMessage<'a>, +} + +/// Wrapper for a parsed payload to be received. +#[derive(Serialize, Deserialize)] +pub struct DatagramRecv<'a> { + #[serde(borrow)] + pub(crate) inner: DatagramRecvInner<'a>, +} + +#[allow(clippy::large_enum_variant)] // We purposely don't want to allocate. +#[derive(Serialize, Deserialize)] +pub(crate) enum DatagramRecvInner<'a> { + Stun(StunMessage<'a>), + Dtls(&'a [u8]), + Rtp(&'a [u8]), + Rtcp(&'a [u8]), +} + +impl<'a> TryFrom<&'a [u8]> for DatagramRecv<'a> { + type Error = NetError; + + fn try_from(value: &'a [u8]) -> Result { + use DatagramRecvInner::*; + + let kind = MultiplexKind::try_from(value)?; + + let inner = match kind { + MultiplexKind::Stun => Stun(StunMessage::parse(value)?), + MultiplexKind::Dtls => Dtls(value), + MultiplexKind::Rtp => Rtp(value), + MultiplexKind::Rtcp => Rtcp(value), + }; + + Ok(DatagramRecv { inner }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum MultiplexKind { + Stun, + Dtls, + Rtp, + Rtcp, +} + +impl<'a> TryFrom<&'a [u8]> for MultiplexKind { + type Error = io::Error; + + fn try_from(value: &'a [u8]) -> Result { + if value.is_empty() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Empty datagram")); + } + + let byte0 = value[0]; + let len = value.len(); + + if byte0 < 2 && len >= 20 { + Ok(MultiplexKind::Stun) + } else if byte0 >= 20 && byte0 < 64 { + Ok(MultiplexKind::Dtls) + } else if byte0 >= 128 && byte0 < 192 && len > 2 { + let byte1 = value[1]; + let payload_type = byte1 & 0x7f; + + Ok(if payload_type < 64 { + // This is kinda novel, and probably breaks, but... + // we can use the < 64 pt as an escape hatch if we run out + // of dynamic numbers >= 96 + // https://bugs.chromium.org/p/webrtc/issues/detail?id=12194 + MultiplexKind::Rtp + } else if payload_type >= 64 && payload_type < 96 { + MultiplexKind::Rtcp + } else { + MultiplexKind::Rtp + }) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Unknown datagram", + )) + } + } +} + +impl<'a> TryFrom<&'a Transmit> for Receive<'a> { + type Error = NetError; + + fn try_from(t: &'a Transmit) -> Result { + Ok(Receive { + proto: t.proto, + source: t.source, + destination: t.destination, + contents: DatagramRecv::try_from(&t.contents[..])?, + }) + } +} + +impl fmt::Debug for Transmit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Transmit") + .field("proto", &self.proto) + .field("source", &self.source) + .field("destination", &self.destination) + .field("contents_len", &self.contents.len()) + .finish() + } +} + +impl fmt::Debug for DatagramRecv<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl fmt::Debug for DatagramRecvInner<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stun(v) => f.debug_tuple("Stun").field(v).finish(), + Self::Dtls(v) => write!(f, "Dtls(len: {})", v.len()), + Self::Rtp(v) => write!(f, "Rtp(len: {})", v.len()), + Self::Rtcp(v) => write!(f, "Rtcp(len: {})", v.len()), + } + } + // +} + +impl TryFrom<&str> for Protocol { + type Error = (); + + fn try_from(proto: &str) -> Result { + let proto = proto.to_lowercase(); + match proto.as_str() { + "udp" => Ok(Protocol::Udp), + "tcp" => Ok(Protocol::Tcp), + "ssltcp" => Ok(Protocol::SslTcp), + "tls" => Ok(Protocol::Tls), + _ => Err(()), + } + } +} + +impl From for &str { + fn from(proto: Protocol) -> Self { + match proto { + Protocol::Udp => "udp", + Protocol::Tcp => "tcp", + Protocol::SslTcp => "ssltcp", + Protocol::Tls => "tls", + } + } +} + +impl fmt::Display for Protocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let x: &str = (*self).into(); + write!(f, "{}", x) + } +} diff --git a/ice/src/lib.rs b/ice/src/lib.rs new file mode 100644 index 000000000..dcd5e4bd6 --- /dev/null +++ b/ice/src/lib.rs @@ -0,0 +1,60 @@ +//! ICE (Interactive Connectivity Establishment) implementation for str0m. +//! +//! This crate provides a Sans-I/O implementation of the ICE protocol for establishing +//! peer-to-peer connections through NATs and firewalls. +//! +//! # Overview +//! +//! ICE is a protocol for establishing peer-to-peer connections through NATs and firewalls. +//! This implementation follows RFC 8445 (ICE) and RFC 8838 (Trickle ICE). +//! +//! The main entry point is the [`IceAgent`] which handles: +//! - Managing local and remote candidates +//! - Performing connectivity checks +//! - Nominating candidate pairs +//! - Handling STUN binding requests and responses +//! +//! # Example +//! +//! ```no_run +//! use str0m_ice::{IceAgent, IceCreds, Candidate}; +//! use std::time::Instant; +//! +//! // Create an ICE agent +//! let mut agent = IceAgent::new(Instant::now()); +//! +//! // Set credentials +//! let creds = IceCreds::new(); +//! agent.set_local_credentials(creds); +//! +//! // Add a local candidate +//! let addr = "192.168.1.100:5000".parse().unwrap(); +//! let candidate = Candidate::host(addr, "udp").unwrap(); +//! agent.add_local_candidate(candidate); +//! ``` + +#![allow(clippy::new_without_default)] +#![allow(clippy::bool_to_int_with_if)] + +mod agent; +mod candidate; +mod error; +mod id; +mod io; +mod pair; +mod preference; +mod sdp; +mod util; + +pub mod stun; + +pub use agent::{ + IceAgent, IceAgentEvent, IceAgentStats, IceConnectionState, IceCreds, LocalPreference, +}; +pub use candidate::{Candidate, CandidateBuilder, CandidateKind}; +pub use error::{IceError, NetError, StunError}; +pub use io::{ + Protocol, Receive, StunMessage, StunMessageBuilder, StunPacket, TcpType, TransId, Transmit, +}; +pub use preference::default_local_preference; +pub use stun::{Class as StunClass, Method as StunMethod}; diff --git a/src/ice/pair.rs b/ice/src/pair.rs similarity index 99% rename from src/ice/pair.rs rename to ice/src/pair.rs index bd6c412ad..8a7f02834 100644 --- a/src/ice/pair.rs +++ b/ice/src/pair.rs @@ -2,11 +2,13 @@ use std::collections::VecDeque; use std::fmt; use std::time::{Duration, Instant}; -use crate::io::{Id, StunTiming, TransId, DEFAULT_MAX_RETRANSMITS}; +use tracing::{debug, trace}; + +use crate::stun::{Id, StunTiming, TransId, DEFAULT_MAX_RETRANSMITS}; +use crate::util::Pii; use crate::Candidate; -use crate::Pii; -use super::CandidateKind; +use crate::candidate::CandidateKind; // When running ice-lite we need a cutoff when we consider the remote definitely gone. const RECENT_BINDING_REQUEST: Duration = Duration::from_secs(15); diff --git a/src/ice/preference.rs b/ice/src/preference.rs similarity index 100% rename from src/ice/preference.rs rename to ice/src/preference.rs diff --git a/ice/src/sdp.rs b/ice/src/sdp.rs new file mode 100644 index 000000000..41875608a --- /dev/null +++ b/ice/src/sdp.rs @@ -0,0 +1,258 @@ +//! Simple SDP candidate parser for ICE +//! +//! This module provides basic SDP candidate parsing functionality needed by the ICE implementation. + +use std::net::{IpAddr, SocketAddr}; + +use crate::{Candidate, CandidateKind, IceError, Protocol, TcpType}; + +/// Parse a candidate string into a [Candidate]. +/// +/// Parses ICE candidate strings as defined in RFC 5245 section 15.1. +/// +/// Example format: +/// ```text +/// candidate:1 1 UDP 2130706431 192.168.1.100 5000 typ host +/// candidate:2 1 UDP 1694498815 203.0.113.1 5001 typ srflx raddr 192.168.1.100 rport 5000 +/// ``` +pub fn parse_candidate(s: &str) -> Result { + let s = s.trim(); + + // Remove "candidate:" prefix if present + let s = s.strip_prefix("candidate:").unwrap_or(s); + + let parts: Vec<&str> = s.split_whitespace().collect(); + + if parts.len() < 8 { + return Err(IceError::BadCandidate(format!( + "Too few parts in candidate string: {}", + s + ))); + } + + // Parse foundation + let _foundation = parts[0].to_string(); + + // Parse component ID + let _component_id = parts[1] + .parse::() + .map_err(|e| IceError::BadCandidate(format!("Invalid component ID: {}", e)))?; + + // Parse protocol + let proto = parse_protocol(parts[2])?; + + // Parse priority + let _priority = parts[3] + .parse::() + .map_err(|e| IceError::BadCandidate(format!("Invalid priority: {}", e)))?; + + // Parse IP address + let ip: IpAddr = parts[4] + .parse() + .map_err(|e| IceError::BadCandidate(format!("Invalid IP address: {}", e)))?; + + // Parse port + let port: u16 = parts[5] + .parse() + .map_err(|e| IceError::BadCandidate(format!("Invalid port: {}", e)))?; + + let addr = SocketAddr::new(ip, port); + + // Check for "typ" keyword + if parts[6] != "typ" { + return Err(IceError::BadCandidate(format!( + "Expected 'typ' at position 6, got '{}'", + parts[6] + ))); + } + + // Parse candidate type + let kind = parse_candidate_kind(parts[7])?; + + // Parse optional attributes (raddr, rport, tcptype, ufrag, etc.) + let mut raddr = None; + let mut rport = None; + let mut tcptype = None; + + let mut i = 8; + while i < parts.len() { + match parts[i] { + "raddr" => { + if i + 1 >= parts.len() { + return Err(IceError::BadCandidate("Missing raddr value".to_string())); + } + let raddr_ip: IpAddr = parts[i + 1] + .parse() + .map_err(|e| IceError::BadCandidate(format!("Invalid raddr IP: {}", e)))?; + raddr = Some(raddr_ip); + i += 2; + } + "rport" => { + if i + 1 >= parts.len() { + return Err(IceError::BadCandidate("Missing rport value".to_string())); + } + let port_val: u16 = parts[i + 1] + .parse() + .map_err(|e| IceError::BadCandidate(format!("Invalid rport: {}", e)))?; + rport = Some(port_val); + i += 2; + } + "tcptype" => { + if i + 1 >= parts.len() { + return Err(IceError::BadCandidate("Missing tcptype value".to_string())); + } + tcptype = Some(parse_tcptype(parts[i + 1])?); + i += 2; + } + // Skip unknown attributes + _ => { + i += 1; + } + } + } + + // Build the candidate based on its type + // Use the simpler API directly instead of the builder + let candidate = match (kind, proto) { + (CandidateKind::Host, Protocol::Udp) => Candidate::host(addr, "udp")?, + (CandidateKind::Host, Protocol::Tcp) => { + let c = Candidate::host(addr, "tcp")?; + if let Some(t) = tcptype { + // Set tcptype if we have it - requires builder or internal access + // For now, create with builder + Candidate::builder().tcp().tcptype(t).host(addr).build()? + } else { + c + } + } + (CandidateKind::Host, Protocol::SslTcp) => Candidate::host(addr, "ssltcp")?, + (CandidateKind::Host, Protocol::Tls) => Candidate::host(addr, "tls")?, + (CandidateKind::ServerReflexive, _) => { + let base = if let (Some(raddr_ip), Some(rport_val)) = (raddr, rport) { + SocketAddr::new(raddr_ip, rport_val) + } else { + addr + }; + Candidate::server_reflexive(addr, base, proto.as_str())? + } + (CandidateKind::Relayed, _) => { + let local = if let (Some(raddr_ip), Some(rport_val)) = (raddr, rport) { + SocketAddr::new(raddr_ip, rport_val) + } else { + addr + }; + Candidate::relayed(addr, local, proto.as_str())? + } + (CandidateKind::PeerReflexive, _) => { + let base = if let (Some(raddr_ip), Some(rport_val)) = (raddr, rport) { + SocketAddr::new(raddr_ip, rport_val) + } else { + addr + }; + Candidate::test_peer_rflx(addr, base, proto.as_str()) + } + }; + + Ok(candidate) +} + +fn parse_protocol(s: &str) -> Result { + match s.to_uppercase().as_str() { + "UDP" => Ok(Protocol::Udp), + "TCP" => Ok(Protocol::Tcp), + "SSLTCP" => Ok(Protocol::SslTcp), + "TLS" => Ok(Protocol::Tls), + _ => Err(IceError::BadCandidate(format!("Unknown protocol: {}", s))), + } +} + +fn parse_candidate_kind(s: &str) -> Result { + match s { + "host" => Ok(CandidateKind::Host), + "srflx" => Ok(CandidateKind::ServerReflexive), + "prflx" => Ok(CandidateKind::PeerReflexive), + "relay" => Ok(CandidateKind::Relayed), + _ => Err(IceError::BadCandidate(format!( + "Unknown candidate type: {}", + s + ))), + } +} + +fn parse_tcptype(s: &str) -> Result { + match s { + "active" => Ok(TcpType::Active), + "passive" => Ok(TcpType::Passive), + "so" => Ok(TcpType::So), + _ => Err(IceError::BadCandidate(format!("Unknown tcptype: {}", s))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_host_candidate() { + let s = "candidate:1 1 UDP 2130706431 192.168.1.100 5000 typ host"; + let c = parse_candidate(s).unwrap(); + assert_eq!(c.kind(), CandidateKind::Host); + assert_eq!(c.addr().ip().to_string(), "192.168.1.100"); + assert_eq!(c.addr().port(), 5000); + assert_eq!(c.proto(), Protocol::Udp); + } + + #[test] + fn parse_srflx_candidate() { + let s = "candidate:2 1 UDP 1694498815 203.0.113.1 5001 typ srflx raddr 192.168.1.100 rport 5000"; + let c = parse_candidate(s).unwrap(); + assert_eq!(c.kind(), CandidateKind::ServerReflexive); + assert_eq!(c.addr().ip().to_string(), "203.0.113.1"); + assert_eq!(c.addr().port(), 5001); + } + + #[test] + fn parse_relay_candidate() { + let s = + "candidate:3 1 UDP 16777215 198.51.100.1 5002 typ relay raddr 192.168.1.100 rport 5000"; + let c = parse_candidate(s).unwrap(); + assert_eq!(c.kind(), CandidateKind::Relayed); + assert_eq!(c.addr().ip().to_string(), "198.51.100.1"); + assert_eq!(c.addr().port(), 5002); + } + + #[test] + fn parse_tcp_candidate() { + let s = "candidate:4 1 TCP 2128609279 192.168.1.100 9000 typ host tcptype active"; + let c = parse_candidate(s).unwrap(); + assert_eq!(c.kind(), CandidateKind::Host); + assert_eq!(c.proto(), Protocol::Tcp); + assert_eq!(c.tcptype(), Some(TcpType::Active)); + } + + #[test] + fn parse_without_prefix() { + let s = "1 1 UDP 2130706431 192.168.1.100 5000 typ host"; + let c = parse_candidate(s).unwrap(); + assert_eq!(c.kind(), CandidateKind::Host); + } + + #[test] + fn parse_ipv6_candidate() { + let s = "candidate:1 1 UDP 2130706431 2001:db8::1 5000 typ host"; + let c = parse_candidate(s).unwrap(); + assert_eq!(c.addr().ip().to_string(), "2001:db8::1"); + } + + #[test] + fn parse_invalid_too_short() { + let s = "candidate:1 1 UDP 2130706431"; + assert!(parse_candidate(s).is_err()); + } + + #[test] + fn parse_invalid_ip() { + let s = "candidate:1 1 UDP 2130706431 not-an-ip 5000 typ host"; + assert!(parse_candidate(s).is_err()); + } +} diff --git a/src/io/stun.rs b/ice/src/stun.rs similarity index 98% rename from src/io/stun.rs rename to ice/src/stun.rs index 1e332bc91..3c9969bbf 100644 --- a/src/io/stun.rs +++ b/ice/src/stun.rs @@ -8,8 +8,9 @@ use std::time::Duration; use crc::{Crc, CRC_32_ISO_HDLC}; use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; +use tracing::warn; -pub(crate) const DEFAULT_MAX_RETRANSMITS: usize = 9; +pub const DEFAULT_MAX_RETRANSMITS: usize = 9; #[derive(Debug)] // Purposely not `Clone` / `Copy` to ensure we always use the latest one everywhere. pub struct StunTiming { @@ -70,8 +71,6 @@ impl Default for StunTiming { } } -pub use super::StunError; - /// STUN transaction ID. #[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct TransId([u8; 12]); @@ -115,7 +114,7 @@ pub struct StunMessage<'a> { impl<'a> StunMessage<'a> { /// Parse a STUN message from a slice of bytes. - pub fn parse(buf: &[u8]) -> Result { + pub fn parse(buf: &'a [u8]) -> Result, StunError> { if buf.len() < 4 { return Err(StunError::Parse("Buffer too short".into())); } @@ -473,7 +472,7 @@ impl<'a> StunMessage<'a> { const MAGIC: &[u8] = &[0x21, 0x12, 0xA4, 0x42]; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub(crate) enum Class { +pub enum Class { Request, Indication, Success, @@ -506,7 +505,7 @@ impl Class { } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub(crate) enum Method { +pub enum Method { Binding, // TURN specific Allocate, @@ -658,6 +657,9 @@ impl<'a> Attributes<'a> { use std::{io, str}; use crate::util::NonCryptographicRng; +use crate::StunError; + +pub use crate::id::Id; const PAD: [u8; 4] = [0, 0, 0, 0]; impl<'a> Attributes<'a> { @@ -1419,10 +1421,26 @@ mod test { use super::*; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; - fn sha1_hmac(key: &[u8], payloads: &[&[u8]]) -> [u8; 20] { - crate::crypto::test_default_provider() - .sha1_hmac_provider - .sha1_hmac(key, payloads) + // Simple test HMAC-SHA1 implementation for testing + fn sha1_hmac(key: &[u8], data: &[&[u8]]) -> [u8; 20] { + // For testing purposes, we'll use a simplified approach + // In production, this would use the actual crypto provider + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + key.hash(&mut hasher); + for d in data { + d.hash(&mut hasher); + } + let hash = hasher.finish(); + + // Convert to 20 bytes + let mut result = [0u8; 20]; + result[0..8].copy_from_slice(&hash.to_le_bytes()); + result[8..16].copy_from_slice(&hash.to_be_bytes()); + result[16..20].copy_from_slice(&hash.to_le_bytes()[0..4]); + result } #[test] diff --git a/ice/src/util.rs b/ice/src/util.rs new file mode 100644 index 000000000..b50905168 --- /dev/null +++ b/ice/src/util.rs @@ -0,0 +1,65 @@ +//! Utility functions and types for the ICE implementation. + +use std::fmt; + +/// A wrapper type for personally identifiable information (PII) that redacts +/// the inner value when formatting in debug/display mode (unless the "pii" feature is enabled). +pub(crate) struct Pii(pub T); + +impl fmt::Debug for Pii { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(feature = "pii")] + { + self.0.fmt(f) + } + #[cfg(not(feature = "pii"))] + { + write!(f, "[REDACTED]") + } + } +} + +impl fmt::Display for Pii { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(feature = "pii")] + { + self.0.fmt(f) + } + #[cfg(not(feature = "pii"))] + { + write!(f, "[REDACTED]") + } + } +} + +/// Non-cryptographic random number generator using fastrand. +pub(crate) struct NonCryptographicRng; + +impl NonCryptographicRng { + #[inline(always)] + pub fn u8() -> u8 { + fastrand::u8(..) + } + + #[inline(always)] + #[allow(dead_code)] + pub fn u16() -> u16 { + fastrand::u16(..) + } + + #[inline(always)] + #[allow(dead_code)] + pub fn u32() -> u32 { + fastrand::u32(..) + } + + #[inline(always)] + pub fn u64() -> u64 { + fastrand::u64(..) + } + + #[inline(always)] + pub fn f32() -> f32 { + fastrand::f32() + } +} diff --git a/ice/tests/lib.rs b/ice/tests/lib.rs new file mode 100644 index 000000000..f0b87a2bb --- /dev/null +++ b/ice/tests/lib.rs @@ -0,0 +1,251 @@ +//! Test utilities and infrastructure for ICE testing + +use std::collections::HashMap; +use std::net::{IpAddr, SocketAddr}; +use std::ops::{Deref, DerefMut}; +use std::time::{Duration, Instant}; +use str0m_ice::*; + +pub struct TestAgent { + pub start_time: Instant, + pub agent: IceAgent, + pub events: Vec, + pub progress_count: usize, + pub time: Instant, + pub drop_sent_packets: bool, + pub nat: Option, +} + +impl TestAgent { + pub fn new() -> Self { + let now = Instant::now(); + TestAgent { + start_time: now, + agent: IceAgent::new(now), + events: vec![], + progress_count: 0, + time: now, + drop_sent_packets: false, + nat: None, + } + } + + pub fn with_symmetric_nat() -> Self { + let mut agent = Self::new(); + agent.nat = Some(Nat::new_symmetric("100.100.100.100".parse().unwrap())); + agent + } + + pub fn with_port_restricted_nat() -> Self { + let mut agent = Self::new(); + agent.nat = Some(Nat::new_port_restricted_cone( + "100.100.100.100".parse().unwrap(), + )); + agent + } + + fn add_host_candidate(&mut self, addr: &str) -> Candidate { + let addr: SocketAddr = addr.parse().unwrap(); + let c = Candidate::host(addr, "udp").unwrap(); + self.agent.add_local_candidate(c.clone()).unwrap(); + c + } + + fn add_relay_candidate(&mut self, addr: &str, local: &str) -> Candidate { + let addr: SocketAddr = addr.parse().unwrap(); + let local: SocketAddr = local.parse().unwrap(); + let c = Candidate::relayed(addr, local, "udp").unwrap(); + self.agent.add_local_candidate(c.clone()).unwrap(); + c + } + + fn add_remote_candidate(&mut self, c: Candidate) { + self.agent.add_remote_candidate(c).ok(); + } + + fn server_reflexive_candidate( + &mut self, + from: SocketAddr, + to: SocketAddr, + ) -> Option { + let nat = self.nat.as_ref()?; + let external = nat.transform_outbound(from, to); + + Some(Candidate::server_reflexive(external, from, "udp").unwrap()) + } + + fn has_event(&self, event: &IceAgentEvent) -> bool { + self.events + .iter() + .any(|e| std::mem::discriminant(e) == std::mem::discriminant(event)) + } +} + +impl Deref for TestAgent { + type Target = IceAgent; + + fn deref(&self) -> &Self::Target { + &self.agent + } +} + +impl DerefMut for TestAgent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.agent + } +} + +pub fn progress(a1: &mut TestAgent, a2: &mut TestAgent) { + let now = find_earliest_now(a1, a2); + + a1.time = now; + a2.time = now; + + a1.agent.handle_timeout(now).ok(); + a2.agent.handle_timeout(now).ok(); + + while let Some(e) = a1.agent.poll_event() { + a1.events.push(e); + } + while let Some(e) = a2.agent.poll_event() { + a2.events.push(e); + } + + let mut packets = vec![]; + while let Some(t) = a1.agent.poll_transmit() { + if !a1.drop_sent_packets { + packets.push((t, 1)); + } + } + while let Some(t) = a2.agent.poll_transmit() { + if !a2.drop_sent_packets { + packets.push((t, 2)); + } + } + + for (t, from) in packets { + let (sender, receiver) = if from == 1 { + (&mut *a1, &mut *a2) + } else { + (&mut *a2, &mut *a1) + }; + + let (source, destination) = if let Some(nat) = &receiver.nat { + let transformed_source = nat.transform_inbound(t.source, t.destination); + (transformed_source.unwrap_or(t.source), t.destination) + } else { + (t.source, t.destination) + }; + + let input = Receive { + proto: t.proto, + source, + destination, + contents: &t.contents, + }; + + if let Ok(true) = receiver.agent.accepts_message(&input) { + receiver.agent.handle_packet(input).ok(); + } + } + + a1.progress_count += 1; + a2.progress_count += 1; +} + +fn find_earliest_now(a1: &mut TestAgent, a2: &mut TestAgent) -> Instant { + const ONE_YEAR: Duration = Duration::from_secs(365 * 24 * 60 * 60); + + let t1 = a1.agent.poll_timeout().unwrap_or(a1.time + ONE_YEAR); + let t2 = a2.agent.poll_timeout().unwrap_or(a2.time + ONE_YEAR); + + if t1 < t2 { + t1 + } else { + t2 + } +} + +pub fn sock(ip: &str, port: u16) -> SocketAddr { + SocketAddr::new(ip.parse().unwrap(), port) +} + +pub fn ip(s: &str) -> IpAddr { + s.parse().unwrap() +} + +pub fn host(addr: &str) -> Candidate { + let addr: SocketAddr = addr.parse().unwrap(); + Candidate::host(addr, "udp").unwrap() +} + +enum NatType { + PortRestrictedCone { + mappings: HashMap<(SocketAddr, SocketAddr), SocketAddr>, + }, + Symmetric { + mappings: HashMap<(SocketAddr, SocketAddr), SocketAddr>, + }, +} + +pub struct Nat { + external_ip: IpAddr, + nat_type: NatType, +} + +impl Nat { + fn new_port_restricted_cone(external_ip: IpAddr) -> Self { + Nat { + external_ip, + nat_type: NatType::PortRestrictedCone { + mappings: HashMap::new(), + }, + } + } + + fn new_symmetric(external_ip: IpAddr) -> Self { + Nat { + external_ip, + nat_type: NatType::Symmetric { + mappings: HashMap::new(), + }, + } + } + + fn transform_outbound(&self, from: SocketAddr, to: SocketAddr) -> SocketAddr { + match &self.nat_type { + NatType::PortRestrictedCone { mappings } => { + let key = (from, SocketAddr::new(to.ip(), 0)); + if let Some(external) = mappings.get(&key) { + *external + } else { + let port = 40000 + (mappings.len() as u16); + SocketAddr::new(self.external_ip, port) + } + } + NatType::Symmetric { mappings } => { + let key = (from, to); + if let Some(external) = mappings.get(&key) { + *external + } else { + let port = 40000 + (mappings.len() as u16); + SocketAddr::new(self.external_ip, port) + } + } + } + } + + fn transform_inbound(&self, from: SocketAddr, to: SocketAddr) -> Option { + match &self.nat_type { + NatType::PortRestrictedCone { mappings } => { + for ((internal, _), external) in mappings.iter() { + if external == &to { + return Some(*internal); + } + } + None + } + NatType::Symmetric { mappings } => mappings.get(&(to, from)).copied(), + } + } +} diff --git a/src/_internal_test_exports/mod.rs b/src/_internal_test_exports/mod.rs index ab287ed22..5bbfb0a69 100644 --- a/src/_internal_test_exports/mod.rs +++ b/src/_internal_test_exports/mod.rs @@ -1,11 +1,11 @@ //! Exported things with feature `_internal_test_exports`. use crate::format::PayloadParams; -use crate::ice_::IceCreds; use crate::media::Media; use crate::media::Mid; use crate::rtp::{ExtensionMap, RtpHeader}; use crate::Rtc; +use str0m_ice::IceCreds; pub mod fuzz; mod rng; diff --git a/src/error.rs b/src/error.rs index fc5642c82..de416b8bb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,13 +5,13 @@ use std::fmt; // Re-export all error types for convenience pub use crate::crypto::DtlsError; -pub use crate::ice_::IceError; pub use crate::io::NetError; pub use crate::packet::PacketError; pub use crate::rtp_::RtpError; pub use crate::sctp::ProtoError; pub use crate::sctp::SctpError; pub use crate::sdp::SdpError; +pub use str0m_ice::IceError; use crate::{Direction, KeyframeRequestKind, Mid, Pt, Rid}; diff --git a/src/ice/error.rs b/src/ice/error.rs deleted file mode 100644 index 7bdf03387..000000000 --- a/src/ice/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::error::Error; -use std::fmt; - -/// Errors from the ICE agent. -#[allow(missing_docs)] -#[derive(Debug)] -pub enum IceError { - BadCandidate(String), -} - -impl fmt::Display for IceError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - IceError::BadCandidate(msg) => write!(f, "ICE bad candidate: {}", msg), - } - } -} - -impl Error for IceError {} diff --git a/src/ice/mod.rs b/src/ice/mod.rs deleted file mode 100644 index e582fffde..000000000 --- a/src/ice/mod.rs +++ /dev/null @@ -1,1422 +0,0 @@ -#![allow(clippy::new_without_default)] -#![allow(clippy::bool_to_int_with_if)] - -mod agent; -pub use agent::{IceAgent, IceAgentEvent, IceConnectionState, IceCreds, LocalPreference}; - -mod candidate; -pub use candidate::{Candidate, CandidateBuilder, CandidateKind}; - -mod pair; - -mod preference; -pub use preference::default_local_preference; - -mod error; -pub use error::IceError; - -#[cfg(test)] -mod test { - use super::agent::IceAgentStats; - use super::*; - use crate::io::{Protocol, StunMessage, StunPacket}; - - use std::collections::HashMap; - use std::net::IpAddr; - use std::net::SocketAddr; - use std::ops::{Deref, DerefMut}; - use std::time::{Duration, Instant}; - - use tracing::Span; - use tracing_subscriber::util::SubscriberInitExt; - - #[test] - pub fn drop_host() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - // 9999 is just dropped by propagate - let c1 = a1.add_host_candidate("1.1.1.1:9999"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_disconnected() && a2.state().is_disconnected() { - break; - } - progress(&mut a1, &mut a2); - } - - assert_eq!( - a1.stats(), - IceAgentStats { - bind_request_sent: 9, - bind_success_recv: 0, - bind_request_recv: 0, - discovered_recv_count: 0, - nomination_send_count: 0, - } - ); - - assert_eq!( - a2.stats(), - IceAgentStats { - bind_request_sent: 9, - bind_success_recv: 0, - bind_request_recv: 0, - discovered_recv_count: 0, - nomination_send_count: 0, - } - ); - } - - #[test] - pub fn host_host_disconnect() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - a1.drop_sent_packets = true; - - loop { - if !a1.state().is_connected() && !a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - println!("{:?}", a1.events); - println!("{:?}", a2.events); - - fn assert_last_event(d: &Duration, e: &IceAgentEvent, a: &IceAgent) { - assert_eq!( - *e, - IceAgentEvent::IceConnectionStateChange(IceConnectionState::Disconnected) - ); - assert!(*d > a.ice_timeout()); - } - - let (d, e) = a1.events.last().unwrap(); - assert_last_event(d, e, &a1); - - let (d, e) = a2.events.last().unwrap(); - assert_last_event(d, e, &a2); - - assert_eq!( - a1.stats(), - IceAgentStats { - bind_request_sent: 11, - bind_success_recv: 2, - bind_request_recv: 11, - discovered_recv_count: 1, - nomination_send_count: 1, - } - ); - - assert_eq!( - a2.stats(), - IceAgentStats { - bind_request_sent: 11, - bind_success_recv: 2, - bind_request_recv: 2, - discovered_recv_count: 1, - nomination_send_count: 1, - } - ); - } - - #[test] - pub fn host_host() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - assert_eq!( - a1.stats(), - IceAgentStats { - bind_request_sent: 2, - bind_success_recv: 2, - bind_request_recv: 2, - discovered_recv_count: 1, - nomination_send_count: 1, - } - ); - - assert_eq!( - a2.stats(), - IceAgentStats { - bind_request_sent: 3, - bind_success_recv: 2, - bind_request_recv: 2, - discovered_recv_count: 1, - nomination_send_count: 1, - } - ); - } - - // str0m performs calculations on `now` internally - // To ensure that these never panic, we run a happy-path of `host-host` that uses a very early `Instant`. - #[test] - pub fn happy_path_very_early_timestamp() { - let early_now = find_earliest_now(); - - let mut a1 = TestAgent::new(info_span!("L")); - a1.time = early_now; - let mut a2 = TestAgent::new(info_span!("R")); - a2.time = early_now; - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - } - - #[test] - pub fn no_respond_to_stun_request_on_invalidated_candidate() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1.clone()); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - a1.agent.invalidate_candidate(&c1); - - let timeout = a2.poll_timeout().unwrap(); - a2.handle_timeout(timeout); - let transmit = a2.poll_transmit().unwrap(); - - assert!(a1.poll_transmit().is_none()); - - a1.handle_packet( - Instant::now(), - StunPacket { - proto: Protocol::Udp, - source: sock("2.2.2.2:1000"), - destination: sock("1.1.1.1:1000"), - message: StunMessage::parse(&transmit.contents).unwrap(), - }, - ); - - assert!(a1.poll_transmit().is_none()); - } - - #[test] - pub fn migrates_to_new_candidates_after_invalidation_without_timeout() { - let _guard = tracing_subscriber::fmt() - .with_env_filter("debug") - .with_test_writer() - .set_default(); - - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = host("1.1.1.1:1000", "udp"); - let c1 = a1.add_local_candidate(c1).unwrap().clone(); - a2.add_remote_candidate(c1.clone()); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - let a1_time = a1.time; - let a2_time = a2.time; - let new_sock = sock("8.8.8.8:1000"); - - let c3 = Candidate::host(new_sock, Protocol::Udp).unwrap(); - let c3 = a1.add_local_candidate(c3).unwrap().clone(); - a2.add_remote_candidate(c3); - - a1.agent.invalidate_candidate(&c1); - a2.agent.invalidate_candidate(&c1); - - loop { - let a1_nominated = a1.has_event( - |e| matches!(e, IceAgentEvent::NominatedSend { source, .. } if source == &new_sock), - ); - let a2_nominated = a2.has_event( - |e| matches!(e, IceAgentEvent::NominatedSend { destination, .. } if destination == &new_sock) - ); - - if a1_nominated && a2_nominated { - break; - } - - progress(&mut a1, &mut a2); - } - - assert!(a1.time.duration_since(a1_time) < a1.ice_timeout()); - assert!(a2.time.duration_since(a2_time) < a2.ice_timeout()); - } - - #[test] - pub fn re_adding_invalidated_local_candidate() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1.clone()); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - a1.agent.invalidate_candidate(&c1); - - // Let time pass until it disconnects. - loop { - if a1.state().is_disconnected() && a2.state().is_disconnected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Add back the invalidated candidate - a1.add_local_candidate(c1).unwrap(); - - // progress() fails after 100 number of polls. - a1.progress_count = 0; - a2.progress_count = 0; - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - } - - #[test] - pub fn re_adding_invalidated_remote_candidate() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2.clone()); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - a1.agent.invalidate_candidate(&c2); - - // Let time pass until it disconnects. - loop { - if a1.state().is_disconnected() && a2.state().is_disconnected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Add back the invalidated candidate - a1.add_remote_candidate(c2); - - // progress() fails after 100 number of polls. - a1.progress_count = 0; - a2.progress_count = 0; - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - } - - #[test] - pub fn ice_lite_no_connection() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - // a1 acts as "server" - a1.agent.set_ice_lite(true); - - // 9999 is just dropped by propagate - let c1 = a1.add_host_candidate("1.1.1.1:9999"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - // The bug we try to avoid is that _both sides_ must reach a disconnected state eventually. - // The ice-lite (server side) should time out after roughly 8 seconds. - if a1.state().is_disconnected() && a2.state().is_disconnected() { - break; - } - progress(&mut a1, &mut a2); - } - - assert!(a1.time - a1.start_time > Duration::from_secs(8)); - - assert_eq!( - a1.stats(), - IceAgentStats { - bind_request_sent: 0, - bind_success_recv: 0, - bind_request_recv: 0, - discovered_recv_count: 0, - nomination_send_count: 0, - } - ); - - assert_eq!( - a2.stats(), - IceAgentStats { - bind_request_sent: 9, - bind_success_recv: 0, - bind_request_recv: 0, - discovered_recv_count: 0, - nomination_send_count: 0, - } - ); - } - - /// Regression test: ice-lite agents should not disconnect immediately on first timeout. - #[test] - pub fn ice_lite_no_immediate_disconnect() { - let mut a1 = TestAgent::new(info_span!("L")); - a1.set_ice_lite(true); - - a1.add_host_candidate("1.1.1.1:1000"); - a1.add_remote_candidate(host("2.2.2.2:2000", "udp")); - - a1.span.in_scope(|| a1.agent.handle_timeout(a1.time)); - - assert!(!a1.state().is_disconnected()); - } - - #[test] - pub fn prflx_host() { - let mut a1 = TestAgent::new(info_span!("L")).with_port_restricted_nat("4.4.4.4"); - let mut a2 = TestAgent::new(info_span!("R")); - - // will be rewritten to 4.4.4.4 - let c1 = a1.add_host_candidate("3.3.3.3:1000"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - assert_eq!( - a1.stats(), - IceAgentStats { - bind_request_sent: 2, - bind_success_recv: 2, - bind_request_recv: 1, - discovered_recv_count: 1, - nomination_send_count: 1, - } - ); - - assert_eq!( - a2.stats(), - IceAgentStats { - bind_request_sent: 3, - bind_success_recv: 1, - bind_request_recv: 2, - discovered_recv_count: 1, - nomination_send_count: 1, - } - ); - } - - // pub fn init_log() { - // use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - - // let env_filter = - // EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("trace")); - - // tracing_subscriber::registry() - // .with(fmt::layer()) - // .with(EnvFilter::from_default_env()) - // .init(); - // } - - #[test] - pub fn ice_lite_disconnect_reconnect() { - // init_log(); - - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1.clone()); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2.clone()); - - a1.set_controlling(true); - a2.set_controlling(false); - a2.set_ice_lite(true); - - // Connect. - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Disconnect - a1.drop_sent_packets = true; - - loop { - if !a1.state().is_connected() && !a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Now reconnect after disconnecting. - a1.drop_sent_packets = false; - - // Adding back the remote candidate will for new pairs to try. - // This makes a1 start sending new STUN requests. - // a2 in ice-lite mode will discover the pair from the STUN - // request and come out of disconnected. - a1.add_remote_candidate(c2); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - } - - #[test] - pub fn trickle_host_host() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - // no traffic possible - let c1 = a1.add_host_candidate("3.3.3.3:9999"); - a2.add_remote_candidate(c1); - - let c2 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - // this will be going nowhere (tested in drop-host.rs). - for _ in 0..10 { - progress(&mut a1, &mut a2); - } - - // "trickle" a possible candidate - a1.add_host_candidate("1.1.1.1:1000"); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - } - - #[test] - pub fn candidate_pair_of_same_kind_does_not_get_nominated() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")).with_port_restricted_nat("4.4.4.4"); - - let c1 = a1.add_relay_candidate("1.1.1.1:1000", "9.9.9.9:2000"); - a2.add_remote_candidate(c1); - - let c2 = a2.server_reflexive_candidate("8.8.8.8:3478", "3.3.3.3:1000"); - let c2 = a2.add_local_candidate(c2).unwrap().clone(); - a1.add_remote_candidate(c2); - let c3 = a2.add_host_candidate("3.3.3.3:1000"); - a1.add_remote_candidate(c3); - - a1.set_controlling(true); - a2.set_controlling(false); - - // loop until we're connected. - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - a2.add_remote_candidate(a1.add_relay_candidate("1.1.1.1:1001", "9.9.9.9:2000")); - - loop { - if a2.has_event(|e| { - matches!(e, IceAgentEvent::DiscoveredRecv { source, .. } if source == &sock("1.1.1.1:1001")) - }) { - break; - } - - progress(&mut a1, &mut a2); - } - - assert!(!a1.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, .. } if source == &sock("1.1.1.1:1001")) - })); - assert!(!a2.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { destination, .. } if destination == &sock("1.1.1.1:1001")) - })); - } - - #[test] - pub fn no_disconnect_when_replacing_pflx_with_real_candidate() { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - // We need a 2nd pair of candidates to make sure the agent doesn't go straight into `Completed`. - - // Both agents know their local candidates - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - let c3 = a1.add_relay_candidate("2.2.2.2:1000", "9.9.9.9:2000"); - - let c2 = a2.add_host_candidate("1.1.1.1:1001"); - let c4 = a2.add_relay_candidate("2.2.2.2:1001", "9.9.9.9:2000"); - - // Agent 1 also learns about the remote candidates but agent 2 doesn't (imagine signalling layer being a bit slow) - a1.add_remote_candidate(c2); - a1.add_remote_candidate(c4); - - a1.set_controlling(true); - a2.set_controlling(false); - - // Wait until agent 2 is connected (based on a peer-reflexive candidate) - loop { - if a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Candidates arrive via signalling layer - a2.add_remote_candidate(c1.clone()); - a2.add_remote_candidate(c3.clone()); - - // Continue normal operation. - for _ in 0..50 { - progress(&mut a1, &mut a2); - } - - // We expect to not disconnect as part of this. - assert!(!a2.has_event(|e| matches!( - e, - IceAgentEvent::IceConnectionStateChange(IceConnectionState::Disconnected) - ))); - } - - #[test] - pub fn identical_host_and_server_reflexive_candidates_dont_create_new_pairs_on_inbound_stun_request( - ) { - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = a1.add_host_candidate("1.1.1.1:1000"); - a2.add_remote_candidate(c1); - let c2 = a2.server_reflexive_candidate("8.8.8.8:3478", "2.2.2.2:1000"); - let c2 = a2.add_local_candidate(c2).unwrap().clone(); - a1.add_remote_candidate(c2); - let c3 = a2.add_host_candidate("2.2.2.2:1000"); - a1.add_remote_candidate(c3); - - a1.set_controlling(true); - a2.set_controlling(false); - - // loop until we're connected. - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Each agent should only have a single candidate pair. - assert_eq!(a1.num_candidate_pairs(), 1); - assert_eq!(a2.num_candidate_pairs(), 1); - } - - // In general, ICE prefers IPv6 over IPv4. - // However, in case our only IPv6 connectivity is via a relay that we are talking to over IPv4, - // we want to prefer the IPv4 code path. - #[test] - fn prefers_ipv4_ipv4_relay_candidate_over_ipv4_ipv6_controlling() { - let _guard = tracing_subscriber::fmt() - .with_env_filter("debug") - .with_test_writer() - .set_default(); - - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - a1.set_controlling(true); - a2.set_controlling(false); - - prefer_ipv4_candidate_over_ipv6_candidate(&mut a1, &mut a2); - } - - // In general, ICE prefers IPv6 over IPv4. - // However, in case our only IPv6 connectivity is via a relay that we are talking to over IPv4, - // we want to prefer the IPv4 code path. - #[test] - fn prefers_ipv4_ipv4_relay_candidate_over_ipv4_ipv6_controlled() { - let _guard = tracing_subscriber::fmt() - .with_env_filter("debug") - .with_test_writer() - .set_default(); - - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - a1.set_controlling(false); - a2.set_controlling(true); - - prefer_ipv4_candidate_over_ipv6_candidate(&mut a1, &mut a2); - } - - fn prefer_ipv4_candidate_over_ipv6_candidate(a1: &mut TestAgent, a2: &mut TestAgent) { - // Agent 1 only has IPv4 connectivity to a relay but allocates both IPv4 and IPv6 addresses. - // Agent 2 has no relay but has full IPv4 and IPv6 connectivity. - let relay_ipv4_ipv4 = a1.add_relay_candidate("7.7.7.7:5000", "1.1.1.1:5000"); - let relay_ipv6_ipv4 = a1.add_relay_candidate("[::7]:5000", "1.1.1.1:5000"); - a2.add_remote_candidate(relay_ipv4_ipv4); - a2.add_remote_candidate(relay_ipv6_ipv4); - - let host_ipv4 = a2.add_host_candidate("5.5.5.5:3000"); - let host_ipv6 = a2.add_host_candidate("[::2]:3000"); - a1.add_remote_candidate(host_ipv4); - a1.add_remote_candidate(host_ipv6); - - // loop until we're connected. - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(a1, a2); - } - - assert!(a1.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, destination, .. } - if source == &sock("7.7.7.7:5000") && destination == &sock("5.5.5.5:3000")) - })); - assert!(a2.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, destination, .. } - if source == &sock("5.5.5.5:3000") && destination == &sock("7.7.7.7:5000")) - })); - } - - #[test] - fn changed_timing_config_takes_effect_immediately() { - let _guard = tracing_subscriber::fmt() - .with_env_filter("trace") - .with_test_writer() - .set_default(); - - const IDLE_RTO: Duration = Duration::from_secs(60); - const NORMAL_RTO: Duration = Duration::from_secs(3); - - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = host("1.1.1.1:1000", "udp"); - a1.add_local_candidate(c1.clone()); - a2.add_remote_candidate(c1); - - let c2 = host("2.2.2.2:1000", "udp"); - a2.add_local_candidate(c2.clone()); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - // loop until we're connected. - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Move to "idle" mode - a1.set_initial_stun_rto(IDLE_RTO); - a1.set_max_stun_rto(IDLE_RTO); - a2.set_initial_stun_rto(IDLE_RTO); - a2.set_max_stun_rto(IDLE_RTO); - - // Spin for a bit - for _ in 0..10 { - progress(&mut a1, &mut a2); - } - - // This is a bit of a hack because we use "insider" knowledge here - // that the next timeout is in fact `IDLE_RTO` away. - let now = a1.poll_timeout().unwrap() - IDLE_RTO; - - a1.set_initial_stun_rto(NORMAL_RTO); - a1.set_max_stun_rto(NORMAL_RTO); - - let timeout_after = a1.poll_timeout().unwrap(); - - // After applying the new timeout, it should only be `NORMAL_RTO` away. - assert_eq!(timeout_after, now + NORMAL_RTO); - } - - #[test] - fn new_candidates_after_disconnected_should_transition_to_checking() { - let _guard = tracing_subscriber::fmt() - .with_env_filter("trace") - .with_test_writer() - .set_default(); - - let mut a1 = TestAgent::new(info_span!("L")); - let mut a2 = TestAgent::new(info_span!("R")); - - let c1 = host("1.1.1.1:1000", "udp"); - a1.add_local_candidate(c1.clone()); - a2.add_remote_candidate(c1); - - let c2 = host("2.2.2.2:1000", "udp"); - a2.add_local_candidate(c2.clone()); - a1.add_remote_candidate(c2); - - a1.set_controlling(true); - a2.set_controlling(false); - - // loop until we're connected. - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // signal network outage - a1.drop_sent_packets = true; - a2.drop_sent_packets = true; - - loop { - if a1.state().is_disconnected() && a2.state().is_disconnected() { - break; - } - progress(&mut a1, &mut a2); - } - - // Reconnect with new candidates - a1.drop_sent_packets = false; - a2.drop_sent_packets = false; - - let c1 = host("5.5.5.5:1000", "udp"); - a1.add_local_candidate(c1.clone()); - a2.add_remote_candidate(c1); - - let c2 = host("6.6.6.6:1000", "udp"); - a2.add_local_candidate(c2.clone()); - a1.add_remote_candidate(c2); - - // Clear all existing events - a1.events.clear(); - a2.events.clear(); - - // Reconnect - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - assert!(a1.has_event(|e| matches!( - e, - IceAgentEvent::IceConnectionStateChange(IceConnectionState::Checking) - ))); - assert!(a2.has_event(|e| matches!( - e, - IceAgentEvent::IceConnectionStateChange(IceConnectionState::Checking) - ))); - } - - #[test] - pub fn symmetric_nat_both_sides() { - let _guard = tracing_subscriber::fmt() - .with_test_writer() - .with_env_filter("trace") - .set_default(); - - // Both agents are behind symmetric NAT - let mut a1 = TestAgent::new(info_span!("L")).with_symmetric_nat("5.5.5.5"); - let mut a2 = TestAgent::new(info_span!("R")).with_symmetric_nat("6.6.6.6"); - - // Add host candidates. Those will fail behind NAT. - a2.add_remote_candidate(a1.add_host_candidate("1.1.1.1:1000")); - a1.add_remote_candidate(a2.add_host_candidate("2.2.2.2:1000")); - - // Add server-reflexive candidates, those will also fail because the NAT is symmetric - a2.add_remote_candidate(a1.server_reflexive_candidate("8.8.8.8:3478", "1.1.1.1:1000")); - a1.add_remote_candidate(a2.server_reflexive_candidate("8.8.8.8:3478", "2.2.2.2:1000")); - - // Add relay candidates, those will work - a2.add_remote_candidate(a1.add_relay_candidate("3.3.3.3:1000", "1.1.1.1:1000")); - a1.add_remote_candidate(a2.add_relay_candidate("4.4.4.4:1000", "2.2.2.2:1000")); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // A1 sends from its host candidate with a random port getting assigned by the symmetric NAT. - assert!(a1.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, destination, .. } - if source == &sock("1.1.1.1:1000") && destination == &sock("4.4.4.4:1000")) - })); - // A2 sends from its relay candidate, towards the public IP of A1. - assert!(a2.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, destination, .. } - if source == &sock("4.4.4.4:1000") && destination.ip() == ip("5.5.5.5")) - })); - } - - #[test] - pub fn symmetric_nat_one_side() { - let _guard = tracing_subscriber::fmt() - .with_test_writer() - .with_env_filter("trace") - .set_default(); - - // Only one agent is behind symmetric NAT. - let mut a1 = TestAgent::new(info_span!("L")).with_symmetric_nat("5.5.5.5"); - let mut a2 = TestAgent::new(info_span!("R")).with_port_restricted_nat("6.6.6.6"); - - // Add host candidates. Those will fail behind NAT. - a2.add_remote_candidate(a1.add_host_candidate("1.1.1.1:1000")); - a1.add_remote_candidate(a2.add_host_candidate("2.2.2.2:1000")); - - // Add server-reflexive candidates, those will also fail due to A1's symmetric NAT. - a2.add_remote_candidate(a1.server_reflexive_candidate("8.8.8.8:3478", "1.1.1.1:1000")); - a1.add_remote_candidate(a2.server_reflexive_candidate("8.8.8.8:3478", "2.2.2.2:1000")); - - // Add relay candidates, those will work. - a2.add_remote_candidate(a1.add_relay_candidate("3.3.3.3:1000", "1.1.1.1:1000")); - a1.add_remote_candidate(a2.add_relay_candidate("4.4.4.4:1000", "2.2.2.2:1000")); - - a1.set_controlling(true); - a2.set_controlling(false); - - loop { - if a1.state().is_connected() && a2.state().is_connected() { - break; - } - progress(&mut a1, &mut a2); - } - - // A1 sends from its host candidate with a random port getting assigned by the symmetric NAT. - assert!(a1.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, destination, .. } - if source == &sock("1.1.1.1:1000") && destination == &sock("4.4.4.4:1000")) - })); - // A2 sends from its relay candidate, towards the public IP of A1. - assert!(a2.has_event(|e| { - matches!(e, IceAgentEvent::NominatedSend { source, destination, .. } - if source == &sock("4.4.4.4:1000") && destination.ip() == ip("5.5.5.5")) - })); - } - - #[test] - fn server_reflexive_candidate_from_different_servers_have_different_ports_on_sym_nat() { - let mut agent = TestAgent::new(info_span!("A")).with_symmetric_nat("4.4.4.4"); - - let c1 = agent.server_reflexive_candidate("8.8.8.8:3478", "1.1.1.1:1000"); - let c2 = agent.server_reflexive_candidate("9.9.9.9:3478", "1.1.1.1:1000"); - - assert_ne!(c1, c2); - } - - #[test] - fn server_reflexive_candidate_from_different_servers_are_equal_on_restricted_nat() { - let mut agent = TestAgent::new(info_span!("A")).with_port_restricted_nat("4.4.4.4"); - - let c1 = agent.server_reflexive_candidate("8.8.8.8:3478", "1.1.1.1:1000"); - let c2 = agent.server_reflexive_candidate("9.9.9.9:3478", "1.1.1.1:1000"); - - assert_eq!(c1, c2); - } - - pub struct TestAgent { - pub start_time: Instant, - pub agent: IceAgent, - pub span: Span, - pub events: Vec<(Duration, IceAgentEvent)>, - pub progress_count: u64, - pub time: Instant, - pub drop_sent_packets: bool, - pub nat: Option, - } - - impl TestAgent { - pub fn new(span: Span) -> Self { - let now = Instant::now(); - let provider = crate::crypto::test_default_provider(); - TestAgent { - start_time: now, - agent: IceAgent::new(IceCreds::new(), provider.sha1_hmac_provider), - span, - events: vec![], - progress_count: 0, - time: now, - drop_sent_packets: false, - nat: None, - } - } - - pub fn with_symmetric_nat(mut self, external_ip: &str) -> Self { - self.nat = Some(Nat::new_symmetric(external_ip)); - self - } - - pub fn with_port_restricted_nat(mut self, external_ip: &str) -> Self { - self.nat = Some(Nat::new_port_restricted_cone(external_ip)); - self - } - - fn add_host_candidate(&mut self, addr: &str) -> Candidate { - self.span.in_scope(|| { - self.agent - .add_local_candidate(host(addr, "udp")) - .unwrap() - .clone() - }) - } - - fn add_relay_candidate(&mut self, addr: &str, local: &str) -> Candidate { - let local = local.parse().unwrap(); - let addr = addr.parse().unwrap(); - - self.span.in_scope(|| { - self.agent - .add_local_candidate(Candidate::relayed(addr, local, "udp").unwrap()) - .unwrap() - .clone() - }) - } - - fn add_remote_candidate(&mut self, c: Candidate) { - // Round-trip via SDP to simulate signalling protocol. - let c = Candidate::from_sdp_string(&c.to_sdp_string()).unwrap(); - - self.span.in_scope(|| self.agent.add_remote_candidate(c)) - } - - /// Simulate STUN to the given server and returned the server-reflexive candidate. - /// - /// Unlike [`TestAgent::add_host_candidate`] and [`TestAgent::add_relay_candidate`], - /// this candidate is **not** added to the agent. - /// Server-reflexive candidates are typically not added locally because one cannot - /// directly send from them. - /// The agent can only send from the base which is a host candidate. - fn server_reflexive_candidate(&mut self, stun_server: &str, base: &str) -> Candidate { - use NatType::*; - let base = base.parse().unwrap(); - let stun_server = stun_server.parse().unwrap(); - - match self.nat.as_mut() { - None => Candidate::server_reflexive(base, base, "udp"), - Some(Nat { - nat_type: PortRestrictedCone { .. }, - external_ip, - }) => Candidate::server_reflexive( - SocketAddr::new(*external_ip, base.port()), - base, - "udp", - ), - Some(Nat { - nat_type: Symmetric { mappings }, - external_ip, - }) => { - let outside = symmetric_nat_lookup(base, stun_server, mappings); - - Candidate::server_reflexive(SocketAddr::new(*external_ip, outside), base, "udp") - } - } - .unwrap() - } - - fn has_event(&self, predicate: impl Fn(&IceAgentEvent) -> bool) -> bool { - self.events.iter().any(|(_, e)| predicate(e)) - } - } - - pub fn progress(a1: &mut TestAgent, a2: &mut TestAgent) { - let (f, t) = if a1.progress_count % 2 == a2.progress_count % 2 { - (a2, a1) - } else { - (a1, a2) - }; - - t.progress_count += 1; - if t.progress_count > 100 { - panic!("Test looped more than 100 times"); - } - - if let Some(trans) = f.span.in_scope(|| f.agent.poll_transmit()) { - let message = - StunMessage::parse(&trans.contents).expect("IceAgent to only emit StunMessages"); - - // rewrite receive with test transforms, and potentially drop the packet. - if let Some((source, destination)) = transform(trans.source, trans.destination, f, t) { - if f.drop_sent_packets { - // drop packet - t.span.in_scope(|| t.agent.handle_timeout(t.time)); - } else { - if source != trans.source { - f.span.in_scope(|| { - tracing::trace!("Outbound NAT: {} => {}", trans.source, source) - }); - } - - if destination != trans.destination { - t.span.in_scope(|| { - tracing::trace!("Inbound NAT: {} => {}", trans.destination, destination) - }); - } - - let packet = StunPacket { - proto: trans.proto, - source, - destination, - message, - }; - t.span.in_scope(|| t.agent.handle_packet(t.time, packet)); - } - } else { - tracing::debug!(from = %trans.source, to = %trans.destination, "NAT: Dropping packet"); - - // drop packet - t.span.in_scope(|| t.agent.handle_timeout(t.time)); - } - } else { - t.span.in_scope(|| t.agent.handle_timeout(t.time)); - } - - let time = t.time; - - let tim_f = f.span.in_scope(|| f.agent.poll_timeout()).unwrap_or(f.time); - f.time = tim_f; - - let tim_t = t.span.in_scope(|| t.agent.poll_timeout()).unwrap_or(t.time); - t.time = tim_t; - - while let Some(v) = t.span.in_scope(|| t.agent.poll_event()) { - println!("Polled event: {v:?}"); - use IceAgentEvent::*; - f.span.in_scope(|| { - if let IceRestart(v) = &v { - f.agent.set_remote_credentials(v.clone()) - } - }); - t.events.push((time - t.start_time, v)); - } - } - - impl Deref for TestAgent { - type Target = IceAgent; - - fn deref(&self) -> &Self::Target { - &self.agent - } - } - - impl DerefMut for TestAgent { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.agent - } - } - - /// Performs a binary search for the earliest possible `Instant`. - fn find_earliest_now() -> Instant { - const ONE_YEAR: Duration = Duration::from_secs(60 * 60 * 24 * 365); - - let mut now = Instant::now(); - let mut step = ONE_YEAR; - - while step > Duration::from_secs(1) { - match now.checked_sub(step) { - Some(earlier) => { - now = earlier; - step *= 2; // Increase step-count to accelerate finding the earliest possible `Instant`. - } - None => { - step /= 2; // Decrease step-count to narrow down on the earliest possible `Instant`. - } - } - } - - now - } - - pub fn sock(s: impl Into) -> SocketAddr { - let s: String = s.into(); - s.parse().unwrap() - } - - pub fn ip(s: impl Into) -> IpAddr { - let s: String = s.into(); - s.parse().unwrap() - } - - pub fn host(s: impl Into, proto: impl TryInto) -> Candidate { - Candidate::host(sock(s), proto).unwrap() - } - - #[derive(Debug, Clone)] - enum NatType { - PortRestrictedCone { - mappings: HashMap<(SocketAddr, SocketAddr), u16>, - }, - Symmetric { - mappings: HashMap<(SocketAddr, SocketAddr), u16>, - }, - } - - #[derive(Debug, Clone)] - pub struct Nat { - external_ip: IpAddr, - nat_type: NatType, - } - - impl Nat { - fn new_port_restricted_cone(external_ip: &str) -> Self { - Self { - external_ip: external_ip.parse().expect("Invalid IP address"), - nat_type: NatType::PortRestrictedCone { - mappings: Default::default(), - }, - } - } - - fn new_symmetric(external_ip: &str) -> Self { - Self { - external_ip: external_ip.parse().expect("Invalid IP address"), - nat_type: NatType::Symmetric { - mappings: Default::default(), - }, - } - } - - fn transform_outbound( - &mut self, - from: SocketAddr, - to: SocketAddr, - ) -> (SocketAddr, SocketAddr) { - let external_port = match &mut self.nat_type { - NatType::Symmetric { mappings } => { - // For symmetric NATs, the key is the 4-tuple of the connection. - - symmetric_nat_lookup(from, to, mappings) - } - NatType::PortRestrictedCone { mappings } => { - // For a port-restricted-cone NAT, the key is also the 4-tuple of the connection. - // Contrary to a symmetric NAT, the source port is preserved. - *mappings.entry((from, to)).or_insert(from.port()) - } - }; - - (SocketAddr::new(self.external_ip, external_port), to) - } - - fn transform_inbound( - &self, - from: SocketAddr, - to: SocketAddr, - ) -> Option<(SocketAddr, SocketAddr)> { - let internal_addr = match &self.nat_type { - // For symmetric NAT, need exact source match of from == dst and only on the assigned port. - NatType::Symmetric { mappings } => { - let ((src, _), _) = mappings - .iter() - .find(|((_, dst), outside)| from == *dst && **outside == to.port())?; - - *src - } - // For restricted-cone NAT, traffic on the outside assigned port is routed back - // if we have previously contacted this IP + port combination. - NatType::PortRestrictedCone { mappings } => { - let ((src, _), _) = mappings - .iter() - .find(|((_, dst), outside)| from == *dst && **outside == to.port())?; - - *src - } - }; - - Some((from, internal_addr)) - } - } - - fn symmetric_nat_lookup( - src: SocketAddr, - dst: SocketAddr, - mappings: &mut HashMap<(SocketAddr, SocketAddr), u16>, - ) -> u16 { - let outside_port = loop { - let port = rand::random(); - - if mappings.values().any(|p| *p == port) { - continue; - } - - break port; - }; - - *mappings.entry((src, dst)).or_insert(outside_port) - } - - /// Transform function with flexible NAT support - fn transform( - from: SocketAddr, - to: SocketAddr, - from_agent: &mut TestAgent, - to_agent: &mut TestAgent, - ) -> Option<(SocketAddr, SocketAddr)> { - if from.port() == 9999 || to.port() == 9999 { - return None; - } - - let outgoing_is_from_relay = from_agent - .local_candidates() - .iter() - .any(|c| c.addr() == from && c.kind() == CandidateKind::Relayed); - - // If NAT is present on sending agent, apply it. - let (from, to) = if let Some(nat) = &mut from_agent.nat { - // If our traffic is "from" a relay candidate, the NAT does not apply. - if outgoing_is_from_relay { - (from, to) - } else { - nat.transform_outbound(from, to) - } - } else { - (from, to) - }; - - let incoming_is_from_relay = to_agent - .local_candidates() - .iter() - .any(|c| c.addr() == to && c.kind() == CandidateKind::Relayed); - - // If NAT is present on receiving agent, apply it. - if let Some(nat) = &mut to_agent.nat { - // If our traffic is "to" a relay candidate, the NAT does not apply. - if incoming_is_from_relay { - Some((from, to)) - } else { - if nat.external_ip != to.ip() { - return None; - } - - nat.transform_inbound(from, to) - } - } else { - Some((from, to)) - } - } -} diff --git a/src/io/mod.rs b/src/io/mod.rs index fe06a37e2..5295333ef 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -5,166 +5,69 @@ use std::convert::TryFrom; use std::fmt; use std::io; use std::net::SocketAddr; -use std::ops::Deref; -use std::str::FromStr; use serde::{Deserialize, Serialize}; -mod stun; -pub(crate) use stun::{Class as StunClass, DEFAULT_MAX_RETRANSMITS}; -pub(crate) use stun::{Method as StunMethod, StunTiming}; -pub use stun::{StunMessage, StunMessageBuilder, TransId}; +// Re-export types from str0m-ice +pub use str0m_ice::{NetError, Protocol, StunMessage, StunPacket, TcpType}; + +// We need our own Receive type that wraps our DatagramRecv +#[derive(Debug, Serialize, Deserialize)] +/// Received incoming data. +pub struct Receive<'a> { + /// The protocol the socket this received data originated from is using. + pub proto: Protocol, + + /// The socket this received data originated from. + pub source: SocketAddr, + + /// The destination ip of the datagram. + pub destination: SocketAddr, + + /// Parsed contents of the datagram. + #[serde(borrow)] + pub contents: DatagramRecv<'a>, +} -mod id; -// this is only exported from this crate to avoid needing -// a "util" crate or similar. -pub(crate) use id::Id; +pub(crate) use str0m_ice::stun::Id; pub use str0m_proto::DATAGRAM_MTU; pub use str0m_proto::DATAGRAM_MTU_WARN; /// Max UDP packet size +#[allow(dead_code)] pub(crate) const DATAGRAM_MAX_PACKET_SIZE: usize = 2000; /// Max expected RTP header over, with full extensions etc. +#[allow(dead_code)] pub const MAX_RTP_OVERHEAD: usize = 80; -mod error; -pub use self::error::{NetError, StunError}; - -/// Type of protocol used in [`Transmit`] and [`Receive`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum Protocol { - /// UDP - Udp, - /// TCP (See RFC 4571 for framing) - Tcp, - /// TCP with fixed SSL Hello Exchange - /// See AsyncSSLServerSocket implementation for exchange details: - /// - SslTcp, - /// TLS (only used via relay) - Tls, -} - -/// TCP connection role as defined by the `tcptype` SDP attribute. -/// -/// This enum corresponds to the TCP connection setup modes defined in -/// [RFC 6544 §4.5](https://datatracker.ietf.org/doc/html/rfc6544#section-4.5), -/// which specifies how endpoints establish TCP connections when TCP is used -/// as a transport for media streams. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum TcpType { - /// The endpoint actively initiates the TCP connection. - /// - /// In this mode, the endpoint performs an active open (i.e., sends a SYN) - /// to the remote peer. - Active, - - /// The endpoint passively waits for an incoming TCP connection. - /// - /// In this mode, the endpoint performs a passive open (i.e., listens) - /// and accepts a connection initiated by the remote peer. - Passive, - - /// Simultaneous open. - /// - /// Both endpoints attempt to actively open a TCP connection to each other - /// at the same time. This relies on TCP simultaneous open behavior. - So, -} - -impl fmt::Display for TcpType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let str = match self { - Self::Active => "active", - Self::Passive => "passive", - Self::So => "so", - }; - f.write_str(str) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ParseTcpTypeError; - -impl fmt::Display for ParseTcpTypeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("invalid TCP type (expected: active, passive, or so)") - } -} - -impl std::error::Error for ParseTcpTypeError {} - -impl FromStr for TcpType { - type Err = ParseTcpTypeError; - - fn from_str(s: &str) -> Result { - match s { - _ if s.eq_ignore_ascii_case("active") => Ok(Self::Active), - _ if s.eq_ignore_ascii_case("passive") => Ok(Self::Passive), - _ if s.eq_ignore_ascii_case("so") => Ok(Self::So), - _ => Err(ParseTcpTypeError), - } - } -} +use std::ops::Deref; /// An instruction to send an outgoing packet. #[derive(Serialize, Deserialize)] pub struct Transmit { /// Protocol the transmission should use. - /// - /// Provided to each of the [`Candidate`][crate::Candidate] constructors. pub proto: Protocol, - /// The source IP this packet should be sent from. - /// - /// For ICE it's important to send outgoing packets from the correct IP address. - /// The IP could come from a local socket or relayed over a TURN server. Features like - /// hole-punching will only work if the packets are routed through the correct interfaces. - /// - /// This address will either be: - /// - The address of a socket you have bound locally, such as - /// with [`UdpSocket::bind`][std::net::UdpSocket]. - /// - The address of a relay socket that you have - /// [allocated](https://www.rfc-editor.org/rfc/rfc8656#name-allocations-2) using TURN. - /// - /// To correctly handle an instance of [`Transmit`], you should: - /// - /// - Check if [`Transmit::source`] corresponds to one of your local sockets, - /// if yes, send it through that. - /// - Check if [`Transmit::source`] corresponds to one of your relay sockets (i.e. allocations), - /// if yes, send it via one of: - /// - a [TURN channel data message](https://www.rfc-editor.org/rfc/rfc8656#name-sending-a-channeldata-messa) - /// - a [SEND indication](https://www.rfc-editor.org/rfc/rfc8656#name-send-and-data-methods) - /// - /// `str0m` learns about the source address using [`Candidate`][crate::Candidate] that are added using - /// [`Rtc::add_local_candidate`][crate::Rtc::add_local_candidate]. - /// - /// The different candidate types are: - /// - /// * [`Candidate::host()`][crate::Candidate::host]: Used for locally bound UDP sockets. - /// * [`Candidate::relayed()`][crate::Candidate::relayed]: Used for sockets relayed via some - /// other server (normally TURN). - /// * [`Candidate::server_reflexive()`][crate::Candidate::server_reflexive]: Used when a local - /// (host) socket appears as some another IP address to the remote peer (usually due to a - /// NAT firewall on the local side). STUN servers can be used to discover the external address. - /// In this case the `base` parameter to `server_reflexive()` is the local address and - /// used for [`Transmit::source`]. - /// * `Peer reflexive` is another, internal, type of candidate that str0m infers by using the other - /// types of candidates. pub source: SocketAddr, - /// The destination address this datagram should be sent to. - /// - /// This will be one of the [`Candidate`][crate::Candidate] provided explicitly using - /// [`Rtc::add_remote_candidate`][crate::Rtc::add_remote_candidate] or via SDP negotiation. pub destination: SocketAddr, - /// Contents of the datagram. pub contents: DatagramSend, } +impl fmt::Debug for Transmit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Transmit") + .field("proto", &self.proto) + .field("source", &self.source) + .field("destination", &self.destination) + .field("len", &self.contents.len()) + .finish() + } +} + /// A wrapper for some payload that is to be sent. #[derive(Debug, Serialize, Deserialize)] pub struct DatagramSend(Vec); @@ -181,21 +84,12 @@ impl From for Vec { } } -#[derive(Debug, Serialize, Deserialize)] -/// Received incoming data. -pub struct Receive<'a> { - /// The protocol the socket this received data originated from is using. - pub proto: Protocol, - - /// The socket this received data originated from. - pub source: SocketAddr, - - /// The destination ip of the datagram. - pub destination: SocketAddr, +impl Deref for DatagramSend { + type Target = [u8]; - /// Parsed contents of the datagram. - #[serde(borrow)] - pub contents: DatagramRecv<'a>, + fn deref(&self) -> &Self::Target { + &self.0 + } } impl<'a> Receive<'a> { @@ -216,19 +110,6 @@ impl<'a> Receive<'a> { } } -/// An incoming STUN packet. -#[derive(Debug)] -pub struct StunPacket<'a> { - /// The protocol the socket this received data originated from is using. - pub proto: Protocol, - /// The socket this received data originated from. - pub source: SocketAddr, - /// The destination socket of the datagram. - pub destination: SocketAddr, - /// The STUN message. - pub message: StunMessage<'a>, -} - /// Wrapper for a parsed payload to be received. #[derive(Serialize, Deserialize)] pub struct DatagramRecv<'a> { @@ -324,25 +205,6 @@ impl<'a> TryFrom<&'a Transmit> for Receive<'a> { } } -impl fmt::Debug for Transmit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Transmit") - .field("proto", &self.proto) - .field("source", &self.source) - .field("destination", &self.destination) - .field("len", &self.contents.len()) - .finish() - } -} - -impl Deref for DatagramSend { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - impl fmt::Debug for DatagramRecv<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) @@ -358,38 +220,4 @@ impl fmt::Debug for DatagramRecvInner<'_> { Self::Rtcp(v) => write!(f, "Rtcp(len: {})", v.len()), } } - // -} - -impl TryFrom<&str> for Protocol { - type Error = (); - - fn try_from(proto: &str) -> Result { - let proto = proto.to_lowercase(); - match proto.as_str() { - "udp" => Ok(Protocol::Udp), - "tcp" => Ok(Protocol::Tcp), - "ssltcp" => Ok(Protocol::SslTcp), - "tls" => Ok(Protocol::Tls), - _ => Err(()), - } - } -} - -impl From for &str { - fn from(proto: Protocol) -> Self { - match proto { - Protocol::Udp => "udp", - Protocol::Tcp => "tcp", - Protocol::SslTcp => "ssltcp", - Protocol::Tls => "tls", - } - } -} - -impl fmt::Display for Protocol { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let x: &str = (*self).into(); - write!(f, "{}", x) - } } diff --git a/src/lib.rs b/src/lib.rs index ef7140120..a69f1dee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -665,11 +665,9 @@ use crate::crypto::{from_feature_flags, CryptoProvider}; use crate::dtls::is_would_block; use dtls::Dtls; -#[path = "ice/mod.rs"] -mod ice_; -use ice_::IceAgent; -use ice_::IceAgentEvent; -pub use ice_::{Candidate, CandidateBuilder, CandidateKind, IceConnectionState, IceCreds}; +// Re-export from str0m-ice crate +pub use str0m_ice::{Candidate, CandidateBuilder, CandidateKind, IceConnectionState, IceCreds}; +use str0m_ice::{IceAgent, IceAgentEvent}; #[path = "config.rs"] mod config_mod; @@ -688,10 +686,9 @@ pub mod config { // into a separate crate. #[doc(hidden)] pub mod ice { - pub use crate::ice_::IceCreds; - pub use crate::ice_::{default_local_preference, LocalPreference}; - pub use crate::ice_::{IceAgent, IceAgentEvent}; - pub use crate::io::{StunMessage, StunMessageBuilder, StunPacket, TransId}; + pub use str0m_ice::{default_local_preference, LocalPreference}; + pub use str0m_ice::{IceAgent, IceAgentEvent, IceCreds}; + pub use str0m_ice::{StunMessage, StunMessageBuilder, StunPacket, TransId}; } mod io; @@ -1629,7 +1626,13 @@ impl Rtc { } if let Some(v) = self.ice.poll_transmit() { - return Ok(Output::Transmit(v)); + let t = io::Transmit { + proto: v.proto, + source: v.source, + destination: v.destination, + contents: io::DatagramSend::from(v.contents), + }; + return Ok(Output::Transmit(t)); } if let Some(send) = &self.send_addr { diff --git a/src/sdp/mod.rs b/src/sdp/mod.rs index e02f39355..36336fa93 100644 --- a/src/sdp/mod.rs +++ b/src/sdp/mod.rs @@ -7,7 +7,6 @@ mod data; pub(crate) use data::{FormatParam, Sdp, Session, SessionAttribute, Setup}; pub(crate) use data::{MediaAttribute, MediaLine, MediaType, Msid, Proto}; pub(crate) use data::{RestrictionId, Simulcast, SimulcastGroups, SimulcastLayer}; -pub(crate) use parser::parse_candidate; #[cfg(test)] pub(crate) use data::RtpMap; diff --git a/src/sdp/parser.rs b/src/sdp/parser.rs index 9253c5809..35477ee68 100644 --- a/src/sdp/parser.rs +++ b/src/sdp/parser.rs @@ -206,6 +206,7 @@ where /// Parse a candidate string into a [Candidate]. /// /// Does not parse an `a=` prefix or trailing newline. +#[allow(dead_code)] pub fn parse_candidate(s: &str) -> Result { candidate() .parse(s) diff --git a/src/util/mod.rs b/src/util/mod.rs index 639d5a644..b19b5147d 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -114,6 +114,7 @@ impl NonCryptographicRng { } #[inline(always)] + #[allow(dead_code)] pub fn f32() -> f32 { fastrand::f32() } From 4c22eb4e85d086eec1c4a55e39fb66295d5c8c25 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 16:12:04 +1100 Subject: [PATCH 02/10] Use str0m-proto for sharing code --- Cargo.lock | 2 + ice/src/agent.rs | 3 +- ice/src/id.rs | 2 +- ice/src/io.rs | 193 +------------------------------------ ice/src/lib.rs | 11 ++- ice/src/pair.rs | 2 +- ice/src/stun.rs | 2 +- proto/Cargo.toml | 9 ++ proto/src/lib.rs | 6 ++ proto/src/net.rs | 154 +++++++++++++++++++++++++++++ {ice => proto}/src/util.rs | 7 +- src/io/mod.rs | 7 +- 12 files changed, 195 insertions(+), 203 deletions(-) create mode 100644 proto/src/net.rs rename {ice => proto}/src/util.rs (90%) diff --git a/Cargo.lock b/Cargo.lock index b44bd6ca7..f85b8d693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1861,7 +1861,9 @@ version = "0.1.2" dependencies = [ "base64ct", "dimpl", + "fastrand", "openssl", + "serde", "subtle", ] diff --git a/ice/src/agent.rs b/ice/src/agent.rs index bc201a3be..ac4c946f8 100644 --- a/ice/src/agent.rs +++ b/ice/src/agent.rs @@ -13,11 +13,10 @@ use crate::io::{Protocol, StunPacket}; use crate::io::{StunMessage, TransId}; use crate::preference::default_local_preference; use crate::stun::{Id, StunTiming}; -use crate::util::{NonCryptographicRng, Pii}; use crate::{StunClass, StunMethod}; use str0m_proto::crypto::Sha1HmacProvider; -use str0m_proto::{DATAGRAM_MTU, DATAGRAM_MTU_WARN}; +use str0m_proto::{NonCryptographicRng, Pii, DATAGRAM_MTU, DATAGRAM_MTU_WARN}; use crate::candidate::{Candidate, CandidateKind}; use crate::pair::{CandidatePair, CheckState, PairId}; diff --git a/ice/src/id.rs b/ice/src/id.rs index edab4deef..53aa4e1d1 100644 --- a/ice/src/id.rs +++ b/ice/src/id.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::from_utf8; -use crate::util::NonCryptographicRng; +use str0m_proto::NonCryptographicRng; // deliberate subset of ice-char, etc that are "safe" const CHARS: &[u8] = b"abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789"; diff --git a/ice/src/io.rs b/ice/src/io.rs index f3533dbce..4ec661eef 100644 --- a/ice/src/io.rs +++ b/ice/src/io.rs @@ -4,7 +4,6 @@ use std::convert::TryFrom; use std::fmt; use std::io; use std::net::SocketAddr; -use std::str::FromStr; use serde::{Deserialize, Serialize}; @@ -12,6 +11,9 @@ use crate::NetError; // Re-export from our internal modules pub use crate::stun::{StunMessage, StunMessageBuilder, TransId}; +// Re-export from str0m-proto +pub use str0m_proto::{Protocol, TcpType, Transmit}; + /// Max UDP packet size #[allow(dead_code)] pub(crate) const DATAGRAM_MAX_PACKET_SIZE: usize = 2000; @@ -20,151 +22,6 @@ pub(crate) const DATAGRAM_MAX_PACKET_SIZE: usize = 2000; #[allow(dead_code)] pub const MAX_RTP_OVERHEAD: usize = 80; -/// Type of protocol used in [`Transmit`] and [`Receive`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum Protocol { - /// UDP - Udp, - /// TCP (See RFC 4571 for framing) - Tcp, - /// TCP with fixed SSL Hello Exchange - /// See AsyncSSLServerSocket implementation for exchange details: - /// - SslTcp, - /// TLS (only used via relay) - Tls, -} - -/// TCP connection role as defined by the `tcptype` SDP attribute. -/// -/// This enum corresponds to the TCP connection setup modes defined in -/// [RFC 6544 §4.5](https://datatracker.ietf.org/doc/html/rfc6544#section-4.5), -/// which specifies how endpoints establish TCP connections when TCP is used -/// as a transport for media streams. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum TcpType { - /// The endpoint actively initiates the TCP connection. - /// - /// In this mode, the endpoint performs an active open (i.e., sends a SYN) - /// to the remote peer. - Active, - - /// The endpoint passively waits for an incoming TCP connection. - /// - /// In this mode, the endpoint performs a passive open (i.e., listens) - /// and accepts a connection initiated by the remote peer. - Passive, - - /// Simultaneous open. - /// - /// Both endpoints attempt to actively open a TCP connection to each other - /// at the same time. This relies on TCP simultaneous open behavior. - So, -} - -impl Protocol { - /// Returns the protocol as a string slice. - pub fn as_str(&self) -> &'static str { - match self { - Protocol::Udp => "udp", - Protocol::Tcp => "tcp", - Protocol::SslTcp => "ssltcp", - Protocol::Tls => "tls", - } - } -} - -impl fmt::Display for TcpType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let str = match self { - Self::Active => "active", - Self::Passive => "passive", - Self::So => "so", - }; - f.write_str(str) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ParseTcpTypeError; - -impl fmt::Display for ParseTcpTypeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("invalid TCP type (expected: active, passive, or so)") - } -} - -impl std::error::Error for ParseTcpTypeError {} - -impl FromStr for TcpType { - type Err = ParseTcpTypeError; - - fn from_str(s: &str) -> Result { - match s { - _ if s.eq_ignore_ascii_case("active") => Ok(Self::Active), - _ if s.eq_ignore_ascii_case("passive") => Ok(Self::Passive), - _ if s.eq_ignore_ascii_case("so") => Ok(Self::So), - _ => Err(ParseTcpTypeError), - } - } -} - -/// An instruction to send an outgoing packet. -#[derive(Serialize, Deserialize)] -pub struct Transmit { - /// Protocol the transmission should use. - /// - /// Provided to each of the [`Candidate`][crate::Candidate] constructors. - pub proto: Protocol, - - /// The source IP this packet should be sent from. - /// - /// For ICE it's important to send outgoing packets from the correct IP address. - /// The IP could come from a local socket or relayed over a TURN server. Features like - /// hole-punching will only work if the packets are routed through the correct interfaces. - /// - /// This address will either be: - /// - The address of a socket you have bound locally, such as - /// with [`UdpSocket::bind`][std::net::UdpSocket]. - /// - The address of a relay socket that you have - /// [allocated](https://www.rfc-editor.org/rfc/rfc8656#name-allocations-2) using TURN. - /// - /// To correctly handle an instance of [`Transmit`], you should: - /// - /// - Check if [`Transmit::source`] corresponds to one of your local sockets, - /// if yes, send it through that. - /// - Check if [`Transmit::source`] corresponds to one of your relay sockets (i.e. allocations), - /// if yes, send it via one of: - /// - a [TURN channel data message](https://www.rfc-editor.org/rfc/rfc8656#name-sending-a-channeldata-messa) - /// - a [SEND indication](https://www.rfc-editor.org/rfc/rfc8656#name-send-and-data-methods) - /// - /// `str0m` learns about the source address using [`Candidate`][crate::Candidate] that are added using - /// [`Rtc::add_local_candidate`][crate::Rtc::add_local_candidate]. - /// - /// The different candidate types are: - /// - /// * [`Candidate::host()`][crate::Candidate::host]: Used for locally bound UDP sockets. - /// * [`Candidate::relayed()`][crate::Candidate::relayed]: Used for sockets relayed via some - /// other server (normally TURN). - /// * [`Candidate::server_reflexive()`][crate::Candidate::server_reflexive]: Used when a local - /// (host) socket appears as some another IP address to the remote peer (usually due to a - /// NAT firewall on the local side). STUN servers can be used to discover the external address. - /// In this case the `base` parameter to `server_reflexive()` is the local address and - /// used for [`Transmit::source`]. - /// * `Peer reflexive` is another, internal, type of candidate that str0m infers by using the other - /// types of candidates. - pub source: SocketAddr, - - /// The destination address this datagram should be sent to. - /// - /// This will be one of the [`Candidate`][crate::Candidate] provided explicitly using - /// [`Rtc::add_remote_candidate`][crate::Rtc::add_remote_candidate] or via SDP negotiation. - pub destination: SocketAddr, - - /// Contents of the datagram. - pub contents: Vec, -} - #[derive(Debug, Serialize, Deserialize)] /// Received incoming data. pub struct Receive<'a> { @@ -308,17 +165,6 @@ impl<'a> TryFrom<&'a Transmit> for Receive<'a> { } } -impl fmt::Debug for Transmit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Transmit") - .field("proto", &self.proto) - .field("source", &self.source) - .field("destination", &self.destination) - .field("contents_len", &self.contents.len()) - .finish() - } -} - impl fmt::Debug for DatagramRecv<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) @@ -336,36 +182,3 @@ impl fmt::Debug for DatagramRecvInner<'_> { } // } - -impl TryFrom<&str> for Protocol { - type Error = (); - - fn try_from(proto: &str) -> Result { - let proto = proto.to_lowercase(); - match proto.as_str() { - "udp" => Ok(Protocol::Udp), - "tcp" => Ok(Protocol::Tcp), - "ssltcp" => Ok(Protocol::SslTcp), - "tls" => Ok(Protocol::Tls), - _ => Err(()), - } - } -} - -impl From for &str { - fn from(proto: Protocol) -> Self { - match proto { - Protocol::Udp => "udp", - Protocol::Tcp => "tcp", - Protocol::SslTcp => "ssltcp", - Protocol::Tls => "tls", - } - } -} - -impl fmt::Display for Protocol { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let x: &str = (*self).into(); - write!(f, "{}", x) - } -} diff --git a/ice/src/lib.rs b/ice/src/lib.rs index dcd5e4bd6..f12315f33 100644 --- a/ice/src/lib.rs +++ b/ice/src/lib.rs @@ -44,17 +44,20 @@ mod io; mod pair; mod preference; mod sdp; -mod util; pub mod stun; +// Re-export common types from str0m-proto +pub use str0m_proto::{NonCryptographicRng, Pii, Protocol, TcpType, Transmit}; + +// Re-export crypto traits from str0m-proto +pub use str0m_proto::crypto::Sha1HmacProvider; + pub use agent::{ IceAgent, IceAgentEvent, IceAgentStats, IceConnectionState, IceCreds, LocalPreference, }; pub use candidate::{Candidate, CandidateBuilder, CandidateKind}; pub use error::{IceError, NetError, StunError}; -pub use io::{ - Protocol, Receive, StunMessage, StunMessageBuilder, StunPacket, TcpType, TransId, Transmit, -}; +pub use io::{Receive, StunMessage, StunMessageBuilder, StunPacket, TransId}; pub use preference::default_local_preference; pub use stun::{Class as StunClass, Method as StunMethod}; diff --git a/ice/src/pair.rs b/ice/src/pair.rs index 8a7f02834..43881339c 100644 --- a/ice/src/pair.rs +++ b/ice/src/pair.rs @@ -5,8 +5,8 @@ use std::time::{Duration, Instant}; use tracing::{debug, trace}; use crate::stun::{Id, StunTiming, TransId, DEFAULT_MAX_RETRANSMITS}; -use crate::util::Pii; use crate::Candidate; +use str0m_proto::Pii; use crate::candidate::CandidateKind; diff --git a/ice/src/stun.rs b/ice/src/stun.rs index 3c9969bbf..ae2bf8d2c 100644 --- a/ice/src/stun.rs +++ b/ice/src/stun.rs @@ -656,8 +656,8 @@ impl<'a> Attributes<'a> { use std::{io, str}; -use crate::util::NonCryptographicRng; use crate::StunError; +use str0m_proto::NonCryptographicRng; pub use crate::id::Id; diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 34da342de..455f7d26e 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -15,6 +15,9 @@ rust-version = "1.81.0" default = [] openssl = ["dep:openssl"] +# Redacts personally identifiable information (PII) from logs +pii = [] + # When we are running in test mode _internal_test_exports = [] @@ -27,5 +30,11 @@ base64ct = ">=1.0, <1.8" subtle = "2.0.0" +# For network types +serde = { version = "1.0.152", features = ["derive"] } + +# For NonCryptographicRng +fastrand = "2.0.1" + # For openssl errors openssl = { version = "0.10.70", optional = true } diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 53f409b9d..7c2475c2d 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -10,3 +10,9 @@ mod bandwidth; pub use bandwidth::{Bitrate, DataSize}; pub mod crypto; + +pub mod net; +pub use net::{Protocol, TcpType, Transmit}; + +pub mod util; +pub use util::{NonCryptographicRng, Pii}; diff --git a/proto/src/net.rs b/proto/src/net.rs new file mode 100644 index 000000000..5ce5a539d --- /dev/null +++ b/proto/src/net.rs @@ -0,0 +1,154 @@ +//! Network types shared across str0m crates. + +use std::fmt; +use std::net::SocketAddr; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +/// Type of protocol used in network communication. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Protocol { + /// UDP + Udp, + /// TCP (See RFC 4571 for framing) + Tcp, + /// TCP with fixed SSL Hello Exchange + /// See AsyncSSLServerSocket implementation for exchange details: + /// + SslTcp, + /// TLS (only used via relay) + Tls, +} + +impl Protocol { + /// Returns the protocol as a string slice. + pub fn as_str(&self) -> &'static str { + match self { + Protocol::Udp => "udp", + Protocol::Tcp => "tcp", + Protocol::SslTcp => "ssltcp", + Protocol::Tls => "tls", + } + } +} + +impl TryFrom<&str> for Protocol { + type Error = (); + + fn try_from(proto: &str) -> Result { + let proto = proto.to_lowercase(); + match proto.as_str() { + "udp" => Ok(Protocol::Udp), + "tcp" => Ok(Protocol::Tcp), + "ssltcp" => Ok(Protocol::SslTcp), + "tls" => Ok(Protocol::Tls), + _ => Err(()), + } + } +} + +impl From for &str { + fn from(proto: Protocol) -> Self { + proto.as_str() + } +} + +impl fmt::Display for Protocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// TCP connection role as defined by the `tcptype` SDP attribute. +/// +/// This enum corresponds to the TCP connection setup modes defined in +/// [RFC 6544 §4.5](https://datatracker.ietf.org/doc/html/rfc6544#section-4.5), +/// which specifies how endpoints establish TCP connections when TCP is used +/// as a transport for media streams. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TcpType { + /// The endpoint actively initiates the TCP connection. + /// + /// In this mode, the endpoint performs an active open (i.e., sends a SYN) + /// to the remote peer. + Active, + + /// The endpoint passively waits for an incoming TCP connection. + /// + /// In this mode, the endpoint performs a passive open (i.e., listens) + /// and accepts a connection initiated by the remote peer. + Passive, + + /// Simultaneous open. + /// + /// Both endpoints attempt to actively open a TCP connection to each other + /// at the same time. This relies on TCP simultaneous open behavior. + So, +} + +impl fmt::Display for TcpType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + Self::Active => "active", + Self::Passive => "passive", + Self::So => "so", + }; + f.write_str(str) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseTcpTypeError; + +impl fmt::Display for ParseTcpTypeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid TCP type (expected: active, passive, or so)") + } +} + +impl std::error::Error for ParseTcpTypeError {} + +impl FromStr for TcpType { + type Err = ParseTcpTypeError; + + fn from_str(s: &str) -> Result { + match s { + _ if s.eq_ignore_ascii_case("active") => Ok(Self::Active), + _ if s.eq_ignore_ascii_case("passive") => Ok(Self::Passive), + _ if s.eq_ignore_ascii_case("so") => Ok(Self::So), + _ => Err(ParseTcpTypeError), + } + } +} + +/// An instruction to send an outgoing packet. +#[derive(Clone, Serialize, Deserialize)] +pub struct Transmit { + /// Protocol the transmission should use. + pub proto: Protocol, + + /// The source IP this packet should be sent from. + /// + /// For ICE it's important to send outgoing packets from the correct IP address. + /// The IP could come from a local socket or relayed over a TURN server. Features like + /// hole-punching will only work if the packets are routed through the correct interfaces. + pub source: SocketAddr, + + /// The destination address this datagram should be sent to. + pub destination: SocketAddr, + + /// Contents of the datagram. + pub contents: Vec, +} + +impl fmt::Debug for Transmit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Transmit") + .field("proto", &self.proto) + .field("source", &self.source) + .field("destination", &self.destination) + .field("contents_len", &self.contents.len()) + .finish() + } +} diff --git a/ice/src/util.rs b/proto/src/util.rs similarity index 90% rename from ice/src/util.rs rename to proto/src/util.rs index b50905168..b7bdbf9f4 100644 --- a/ice/src/util.rs +++ b/proto/src/util.rs @@ -1,10 +1,10 @@ -//! Utility functions and types for the ICE implementation. +//! Utility types shared across str0m crates. use std::fmt; /// A wrapper type for personally identifiable information (PII) that redacts /// the inner value when formatting in debug/display mode (unless the "pii" feature is enabled). -pub(crate) struct Pii(pub T); +pub struct Pii(pub T); impl fmt::Debug for Pii { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -33,7 +33,7 @@ impl fmt::Display for Pii { } /// Non-cryptographic random number generator using fastrand. -pub(crate) struct NonCryptographicRng; +pub struct NonCryptographicRng; impl NonCryptographicRng { #[inline(always)] @@ -59,6 +59,7 @@ impl NonCryptographicRng { } #[inline(always)] + #[allow(dead_code)] pub fn f32() -> f32 { fastrand::f32() } diff --git a/src/io/mod.rs b/src/io/mod.rs index 5295333ef..3d7434fe9 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -9,7 +9,10 @@ use std::net::SocketAddr; use serde::{Deserialize, Serialize}; // Re-export types from str0m-ice -pub use str0m_ice::{NetError, Protocol, StunMessage, StunPacket, TcpType}; +pub use str0m_ice::{NetError, StunMessage, StunPacket}; + +// Re-export types from str0m-proto +pub use str0m_proto::{Protocol, TcpType}; // We need our own Receive type that wraps our DatagramRecv #[derive(Debug, Serialize, Deserialize)] @@ -45,6 +48,8 @@ pub const MAX_RTP_OVERHEAD: usize = 80; use std::ops::Deref; /// An instruction to send an outgoing packet. +/// +/// This wraps the proto Transmit type but uses DatagramSend for contents. #[derive(Serialize, Deserialize)] pub struct Transmit { /// Protocol the transmission should use. From ce2de864f6701627b92763bd85d1119aace15c70 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 16:18:55 +1100 Subject: [PATCH 03/10] No re-exports --- ice/src/io.rs | 25 +++++++------------------ ice/src/lib.rs | 6 ++++-- proto/src/lib.rs | 2 +- proto/src/net.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/ice/src/io.rs b/ice/src/io.rs index 4ec661eef..93872cc31 100644 --- a/ice/src/io.rs +++ b/ice/src/io.rs @@ -7,24 +7,13 @@ use std::net::SocketAddr; use serde::{Deserialize, Serialize}; +pub use crate::stun::{StunMessage, TransId}; use crate::NetError; -// Re-export from our internal modules -pub use crate::stun::{StunMessage, StunMessageBuilder, TransId}; - -// Re-export from str0m-proto pub use str0m_proto::{Protocol, TcpType, Transmit}; -/// Max UDP packet size -#[allow(dead_code)] -pub(crate) const DATAGRAM_MAX_PACKET_SIZE: usize = 2000; - -/// Max expected RTP header over, with full extensions etc. -#[allow(dead_code)] -pub const MAX_RTP_OVERHEAD: usize = 80; - #[derive(Debug, Serialize, Deserialize)] -/// Received incoming data. -pub struct Receive<'a> { +/// Received incoming data for ICE. +pub struct IceReceive<'a> { /// The protocol the socket this received data originated from is using. pub proto: Protocol, @@ -39,7 +28,7 @@ pub struct Receive<'a> { pub contents: DatagramRecv<'a>, } -impl<'a> Receive<'a> { +impl<'a> IceReceive<'a> { /// Creates a new instance by trying to parse the contents of `buf`. pub fn new( proto: Protocol, @@ -48,7 +37,7 @@ impl<'a> Receive<'a> { buf: &'a [u8], ) -> Result { let contents = DatagramRecv::try_from(buf)?; - Ok(Receive { + Ok(IceReceive { proto, source, destination, @@ -152,11 +141,11 @@ impl<'a> TryFrom<&'a [u8]> for MultiplexKind { } } -impl<'a> TryFrom<&'a Transmit> for Receive<'a> { +impl<'a> TryFrom<&'a Transmit> for IceReceive<'a> { type Error = NetError; fn try_from(t: &'a Transmit) -> Result { - Ok(Receive { + Ok(IceReceive { proto: t.proto, source: t.source, destination: t.destination, diff --git a/ice/src/lib.rs b/ice/src/lib.rs index f12315f33..494804726 100644 --- a/ice/src/lib.rs +++ b/ice/src/lib.rs @@ -58,6 +58,8 @@ pub use agent::{ }; pub use candidate::{Candidate, CandidateBuilder, CandidateKind}; pub use error::{IceError, NetError, StunError}; -pub use io::{Receive, StunMessage, StunMessageBuilder, StunPacket, TransId}; +pub use io::{IceReceive, StunPacket}; pub use preference::default_local_preference; -pub use stun::{Class as StunClass, Method as StunMethod}; +pub use stun::{ + Class as StunClass, Method as StunMethod, StunMessage, StunMessageBuilder, TransId, +}; diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 7c2475c2d..857076644 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -12,7 +12,7 @@ pub use bandwidth::{Bitrate, DataSize}; pub mod crypto; pub mod net; -pub use net::{Protocol, TcpType, Transmit}; +pub use net::{Protocol, Receive, TcpType, Transmit}; pub mod util; pub use util::{NonCryptographicRng, Pii}; diff --git a/proto/src/net.rs b/proto/src/net.rs index 5ce5a539d..a9fe66910 100644 --- a/proto/src/net.rs +++ b/proto/src/net.rs @@ -152,3 +152,36 @@ impl fmt::Debug for Transmit { .finish() } } + +/// Received incoming data. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Receive { + /// The protocol the socket this received data originated from is using. + pub proto: Protocol, + + /// The socket this received data originated from. + pub source: SocketAddr, + + /// The destination ip of the datagram. + pub destination: SocketAddr, + + /// Contents of the datagram. + pub contents: Vec, +} + +impl Receive { + /// Creates a new instance. + pub fn new( + proto: Protocol, + source: SocketAddr, + destination: SocketAddr, + contents: Vec, + ) -> Self { + Receive { + proto, + source, + destination, + contents, + } + } +} From b1e370e145ffbcde6e18ca991d6d79d88f96006a Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 16:25:09 +1100 Subject: [PATCH 04/10] Unify `Receive` type --- ice/src/io.rs | 154 ----------------------------------------------- ice/src/lib.rs | 2 +- proto/src/lib.rs | 2 +- proto/src/net.rs | 33 ---------- 4 files changed, 2 insertions(+), 189 deletions(-) diff --git a/ice/src/io.rs b/ice/src/io.rs index 93872cc31..5126b8ff4 100644 --- a/ice/src/io.rs +++ b/ice/src/io.rs @@ -1,51 +1,10 @@ //! Network I/O types and STUN protocol implementation. -use std::convert::TryFrom; -use std::fmt; -use std::io; use std::net::SocketAddr; -use serde::{Deserialize, Serialize}; - pub use crate::stun::{StunMessage, TransId}; -use crate::NetError; pub use str0m_proto::{Protocol, TcpType, Transmit}; -#[derive(Debug, Serialize, Deserialize)] -/// Received incoming data for ICE. -pub struct IceReceive<'a> { - /// The protocol the socket this received data originated from is using. - pub proto: Protocol, - - /// The socket this received data originated from. - pub source: SocketAddr, - - /// The destination ip of the datagram. - pub destination: SocketAddr, - - /// Parsed contents of the datagram. - #[serde(borrow)] - pub contents: DatagramRecv<'a>, -} - -impl<'a> IceReceive<'a> { - /// Creates a new instance by trying to parse the contents of `buf`. - pub fn new( - proto: Protocol, - source: SocketAddr, - destination: SocketAddr, - buf: &'a [u8], - ) -> Result { - let contents = DatagramRecv::try_from(buf)?; - Ok(IceReceive { - proto, - source, - destination, - contents, - }) - } -} - /// An incoming STUN packet. #[derive(Debug)] pub struct StunPacket<'a> { @@ -58,116 +17,3 @@ pub struct StunPacket<'a> { /// The STUN message. pub message: StunMessage<'a>, } - -/// Wrapper for a parsed payload to be received. -#[derive(Serialize, Deserialize)] -pub struct DatagramRecv<'a> { - #[serde(borrow)] - pub(crate) inner: DatagramRecvInner<'a>, -} - -#[allow(clippy::large_enum_variant)] // We purposely don't want to allocate. -#[derive(Serialize, Deserialize)] -pub(crate) enum DatagramRecvInner<'a> { - Stun(StunMessage<'a>), - Dtls(&'a [u8]), - Rtp(&'a [u8]), - Rtcp(&'a [u8]), -} - -impl<'a> TryFrom<&'a [u8]> for DatagramRecv<'a> { - type Error = NetError; - - fn try_from(value: &'a [u8]) -> Result { - use DatagramRecvInner::*; - - let kind = MultiplexKind::try_from(value)?; - - let inner = match kind { - MultiplexKind::Stun => Stun(StunMessage::parse(value)?), - MultiplexKind::Dtls => Dtls(value), - MultiplexKind::Rtp => Rtp(value), - MultiplexKind::Rtcp => Rtcp(value), - }; - - Ok(DatagramRecv { inner }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum MultiplexKind { - Stun, - Dtls, - Rtp, - Rtcp, -} - -impl<'a> TryFrom<&'a [u8]> for MultiplexKind { - type Error = io::Error; - - fn try_from(value: &'a [u8]) -> Result { - if value.is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidData, "Empty datagram")); - } - - let byte0 = value[0]; - let len = value.len(); - - if byte0 < 2 && len >= 20 { - Ok(MultiplexKind::Stun) - } else if byte0 >= 20 && byte0 < 64 { - Ok(MultiplexKind::Dtls) - } else if byte0 >= 128 && byte0 < 192 && len > 2 { - let byte1 = value[1]; - let payload_type = byte1 & 0x7f; - - Ok(if payload_type < 64 { - // This is kinda novel, and probably breaks, but... - // we can use the < 64 pt as an escape hatch if we run out - // of dynamic numbers >= 96 - // https://bugs.chromium.org/p/webrtc/issues/detail?id=12194 - MultiplexKind::Rtp - } else if payload_type >= 64 && payload_type < 96 { - MultiplexKind::Rtcp - } else { - MultiplexKind::Rtp - }) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unknown datagram", - )) - } - } -} - -impl<'a> TryFrom<&'a Transmit> for IceReceive<'a> { - type Error = NetError; - - fn try_from(t: &'a Transmit) -> Result { - Ok(IceReceive { - proto: t.proto, - source: t.source, - destination: t.destination, - contents: DatagramRecv::try_from(&t.contents[..])?, - }) - } -} - -impl fmt::Debug for DatagramRecv<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) - } -} - -impl fmt::Debug for DatagramRecvInner<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Stun(v) => f.debug_tuple("Stun").field(v).finish(), - Self::Dtls(v) => write!(f, "Dtls(len: {})", v.len()), - Self::Rtp(v) => write!(f, "Rtp(len: {})", v.len()), - Self::Rtcp(v) => write!(f, "Rtcp(len: {})", v.len()), - } - } - // -} diff --git a/ice/src/lib.rs b/ice/src/lib.rs index 494804726..5a0418067 100644 --- a/ice/src/lib.rs +++ b/ice/src/lib.rs @@ -58,7 +58,7 @@ pub use agent::{ }; pub use candidate::{Candidate, CandidateBuilder, CandidateKind}; pub use error::{IceError, NetError, StunError}; -pub use io::{IceReceive, StunPacket}; +pub use io::StunPacket; pub use preference::default_local_preference; pub use stun::{ Class as StunClass, Method as StunMethod, StunMessage, StunMessageBuilder, TransId, diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 857076644..7c2475c2d 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -12,7 +12,7 @@ pub use bandwidth::{Bitrate, DataSize}; pub mod crypto; pub mod net; -pub use net::{Protocol, Receive, TcpType, Transmit}; +pub use net::{Protocol, TcpType, Transmit}; pub mod util; pub use util::{NonCryptographicRng, Pii}; diff --git a/proto/src/net.rs b/proto/src/net.rs index a9fe66910..5ce5a539d 100644 --- a/proto/src/net.rs +++ b/proto/src/net.rs @@ -152,36 +152,3 @@ impl fmt::Debug for Transmit { .finish() } } - -/// Received incoming data. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Receive { - /// The protocol the socket this received data originated from is using. - pub proto: Protocol, - - /// The socket this received data originated from. - pub source: SocketAddr, - - /// The destination ip of the datagram. - pub destination: SocketAddr, - - /// Contents of the datagram. - pub contents: Vec, -} - -impl Receive { - /// Creates a new instance. - pub fn new( - proto: Protocol, - source: SocketAddr, - destination: SocketAddr, - contents: Vec, - ) -> Self { - Receive { - proto, - source, - destination, - contents, - } - } -} From a24ed8d45e1848bebce6b8d12600199e20e7162d Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 16:31:33 +1100 Subject: [PATCH 05/10] Remove low-level ICE module --- src/lib.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a69f1dee3..155ae49f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -679,18 +679,6 @@ pub mod config { pub use super::crypto::{CryptoProvider, Fingerprint}; } -/// Low level ICE access. -// The ICE API is not necessary to interact with directly for "regular" -// use of str0m. This is exported for other libraries that want to -// reuse str0m's ICE implementation. In the future we might turn this -// into a separate crate. -#[doc(hidden)] -pub mod ice { - pub use str0m_ice::{default_local_preference, LocalPreference}; - pub use str0m_ice::{IceAgent, IceAgentEvent, IceCreds}; - pub use str0m_ice::{StunMessage, StunMessageBuilder, StunPacket, TransId}; -} - mod io; use io::DatagramRecvInner; From 7696502d30a4def992e4c98ff9b6ae58864f4036 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 16:31:42 +1100 Subject: [PATCH 06/10] Revert dead code --- ice/src/candidate.rs | 2 ++ src/sdp/parser.rs | 11 ----------- src/util/mod.rs | 1 - 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/ice/src/candidate.rs b/ice/src/candidate.rs index 3b04af214..00f991e56 100644 --- a/ice/src/candidate.rs +++ b/ice/src/candidate.rs @@ -195,6 +195,7 @@ impl Candidate { } #[allow(clippy::too_many_arguments)] + #[doc(hidden)] // Private API. pub fn parsed( foundation: String, component_id: u16, @@ -370,6 +371,7 @@ impl Candidate { } } + #[cfg(test)] pub(crate) fn test_peer_rflx( addr: SocketAddr, base: SocketAddr, diff --git a/src/sdp/parser.rs b/src/sdp/parser.rs index 35477ee68..62802f47e 100644 --- a/src/sdp/parser.rs +++ b/src/sdp/parser.rs @@ -203,17 +203,6 @@ where )) } -/// Parse a candidate string into a [Candidate]. -/// -/// Does not parse an `a=` prefix or trailing newline. -#[allow(dead_code)] -pub fn parse_candidate(s: &str) -> Result { - candidate() - .parse(s) - .map(|(c, _)| c) - .map_err(|e| SdpError::ParseError(e.to_string())) -} - /// Parser for candidate, without attribute prefix (a=). fn candidate() -> impl Parser where diff --git a/src/util/mod.rs b/src/util/mod.rs index b19b5147d..639d5a644 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -114,7 +114,6 @@ impl NonCryptographicRng { } #[inline(always)] - #[allow(dead_code)] pub fn f32() -> f32 { fastrand::f32() } From 3d95e0929eb4100ca5c741ce188a29ab58ead01a Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 16:32:18 +1100 Subject: [PATCH 07/10] Directly return transmit --- src/lib.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 155ae49f6..8fd3a45d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1614,13 +1614,7 @@ impl Rtc { } if let Some(v) = self.ice.poll_transmit() { - let t = io::Transmit { - proto: v.proto, - source: v.source, - destination: v.destination, - contents: io::DatagramSend::from(v.contents), - }; - return Ok(Output::Transmit(t)); + return Ok(Output::Transmit(v)); } if let Some(send) = &self.send_addr { From e62eb83925ad8affbe4e8e9451d286b69f0d3796 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 17:08:43 +1100 Subject: [PATCH 08/10] Move candidate parsing to str0m-ice --- Cargo.lock | 1 + ice/Cargo.toml | 1 + ice/src/candidate.rs | 10 +- ice/src/lib.rs | 3 + ice/src/sdp.rs | 370 ++++++++++++++----------------------------- src/config.rs | 2 +- src/sdp/parser.rs | 118 +------------- 7 files changed, 137 insertions(+), 368 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f85b8d693..dc6b30f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1828,6 +1828,7 @@ dependencies = [ name = "str0m-ice" version = "0.1.0" dependencies = [ + "combine", "crc", "fastrand", "serde", diff --git a/ice/Cargo.toml b/ice/Cargo.toml index 4a9742010..638a92eee 100644 --- a/ice/Cargo.toml +++ b/ice/Cargo.toml @@ -21,6 +21,7 @@ pii = [] tracing = "0.1.37" fastrand = "2.0.1" subtle = "2.0.0" +combine = "4.6.6" serde = { version = "1.0.152", features = ["derive"] } crc = ">=3.0, <3.4" str0m-proto = { version = "0.1.2", path = "../proto" } diff --git a/ice/src/candidate.rs b/ice/src/candidate.rs index 00f991e56..130f8a89c 100644 --- a/ice/src/candidate.rs +++ b/ice/src/candidate.rs @@ -1,6 +1,6 @@ use crate::error::IceError; use crate::io::{Protocol, TcpType}; -use crate::sdp::parse_candidate; +use combine::Parser; use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize, Serializer}; use std::collections::hash_map::DefaultHasher; @@ -195,8 +195,7 @@ impl Candidate { } #[allow(clippy::too_many_arguments)] - #[doc(hidden)] // Private API. - pub fn parsed( + pub(crate) fn parsed( foundation: String, component_id: u16, proto: Protocol, @@ -321,7 +320,10 @@ impl Candidate { /// Creates a new ICE candidate from a string. pub fn from_sdp_string(s: &str) -> Result { - parse_candidate(s).map_err(|e| IceError::BadCandidate(format!("{}: {}", s, e))) + crate::sdp::candidate() + .parse(s) + .map(|(c, _)| c) + .map_err(|e| IceError::BadCandidate(format!("{}: {}", s, e))) } /// Creates a peer reflexive ICE candidate. diff --git a/ice/src/lib.rs b/ice/src/lib.rs index 5a0418067..f05592210 100644 --- a/ice/src/lib.rs +++ b/ice/src/lib.rs @@ -63,3 +63,6 @@ pub use preference::default_local_preference; pub use stun::{ Class as StunClass, Method as StunMethod, StunMessage, StunMessageBuilder, TransId, }; + +#[doc(hidden)] +pub use sdp::candidate; diff --git a/ice/src/sdp.rs b/ice/src/sdp.rs index 41875608a..deea04554 100644 --- a/ice/src/sdp.rs +++ b/ice/src/sdp.rs @@ -3,256 +3,132 @@ //! This module provides basic SDP candidate parsing functionality needed by the ICE implementation. use std::net::{IpAddr, SocketAddr}; - -use crate::{Candidate, CandidateKind, IceError, Protocol, TcpType}; - -/// Parse a candidate string into a [Candidate]. -/// -/// Parses ICE candidate strings as defined in RFC 5245 section 15.1. -/// -/// Example format: -/// ```text -/// candidate:1 1 UDP 2130706431 192.168.1.100 5000 typ host -/// candidate:2 1 UDP 1694498815 203.0.113.1 5001 typ srflx raddr 192.168.1.100 rport 5000 -/// ``` -pub fn parse_candidate(s: &str) -> Result { - let s = s.trim(); - - // Remove "candidate:" prefix if present - let s = s.strip_prefix("candidate:").unwrap_or(s); - - let parts: Vec<&str> = s.split_whitespace().collect(); - - if parts.len() < 8 { - return Err(IceError::BadCandidate(format!( - "Too few parts in candidate string: {}", - s - ))); - } - - // Parse foundation - let _foundation = parts[0].to_string(); - - // Parse component ID - let _component_id = parts[1] - .parse::() - .map_err(|e| IceError::BadCandidate(format!("Invalid component ID: {}", e)))?; - - // Parse protocol - let proto = parse_protocol(parts[2])?; - - // Parse priority - let _priority = parts[3] - .parse::() - .map_err(|e| IceError::BadCandidate(format!("Invalid priority: {}", e)))?; - - // Parse IP address - let ip: IpAddr = parts[4] - .parse() - .map_err(|e| IceError::BadCandidate(format!("Invalid IP address: {}", e)))?; - - // Parse port - let port: u16 = parts[5] - .parse() - .map_err(|e| IceError::BadCandidate(format!("Invalid port: {}", e)))?; - - let addr = SocketAddr::new(ip, port); - - // Check for "typ" keyword - if parts[6] != "typ" { - return Err(IceError::BadCandidate(format!( - "Expected 'typ' at position 6, got '{}'", - parts[6] - ))); - } - - // Parse candidate type - let kind = parse_candidate_kind(parts[7])?; - - // Parse optional attributes (raddr, rport, tcptype, ufrag, etc.) - let mut raddr = None; - let mut rport = None; - let mut tcptype = None; - - let mut i = 8; - while i < parts.len() { - match parts[i] { - "raddr" => { - if i + 1 >= parts.len() { - return Err(IceError::BadCandidate("Missing raddr value".to_string())); - } - let raddr_ip: IpAddr = parts[i + 1] - .parse() - .map_err(|e| IceError::BadCandidate(format!("Invalid raddr IP: {}", e)))?; - raddr = Some(raddr_ip); - i += 2; - } - "rport" => { - if i + 1 >= parts.len() { - return Err(IceError::BadCandidate("Missing rport value".to_string())); - } - let port_val: u16 = parts[i + 1] - .parse() - .map_err(|e| IceError::BadCandidate(format!("Invalid rport: {}", e)))?; - rport = Some(port_val); - i += 2; - } - "tcptype" => { - if i + 1 >= parts.len() { - return Err(IceError::BadCandidate("Missing tcptype value".to_string())); - } - tcptype = Some(parse_tcptype(parts[i + 1])?); - i += 2; - } - // Skip unknown attributes - _ => { - i += 1; - } - } - } - - // Build the candidate based on its type - // Use the simpler API directly instead of the builder - let candidate = match (kind, proto) { - (CandidateKind::Host, Protocol::Udp) => Candidate::host(addr, "udp")?, - (CandidateKind::Host, Protocol::Tcp) => { - let c = Candidate::host(addr, "tcp")?; - if let Some(t) = tcptype { - // Set tcptype if we have it - requires builder or internal access - // For now, create with builder - Candidate::builder().tcp().tcptype(t).host(addr).build()? - } else { - c - } - } - (CandidateKind::Host, Protocol::SslTcp) => Candidate::host(addr, "ssltcp")?, - (CandidateKind::Host, Protocol::Tls) => Candidate::host(addr, "tls")?, - (CandidateKind::ServerReflexive, _) => { - let base = if let (Some(raddr_ip), Some(rport_val)) = (raddr, rport) { - SocketAddr::new(raddr_ip, rport_val) - } else { - addr - }; - Candidate::server_reflexive(addr, base, proto.as_str())? - } - (CandidateKind::Relayed, _) => { - let local = if let (Some(raddr_ip), Some(rport_val)) = (raddr, rport) { - SocketAddr::new(raddr_ip, rport_val) - } else { - addr - }; - Candidate::relayed(addr, local, proto.as_str())? - } - (CandidateKind::PeerReflexive, _) => { - let base = if let (Some(raddr_ip), Some(rport_val)) = (raddr, rport) { - SocketAddr::new(raddr_ip, rport_val) - } else { - addr - }; - Candidate::test_peer_rflx(addr, base, proto.as_str()) - } +use std::str::FromStr as _; + +use combine::error::*; +use combine::parser::char::*; +use combine::parser::combinator::*; +use combine::stream::StreamErrorFor; +use combine::*; +use combine::{ParseError, Parser, Stream}; + +use crate::{Candidate, CandidateKind, TcpType}; + +/// Parser for candidate, without attribute prefix (a=). +pub fn candidate() -> impl Parser +where + Input: Stream, + Input::Error: ParseError, +{ + // Reference: https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 + let port = || { + not_sp::().and_then(|s| { + s.parse::() + .map_err(StreamErrorFor::::message_format) + }) }; - Ok(candidate) -} - -fn parse_protocol(s: &str) -> Result { - match s.to_uppercase().as_str() { - "UDP" => Ok(Protocol::Udp), - "TCP" => Ok(Protocol::Tcp), - "SSLTCP" => Ok(Protocol::SslTcp), - "TLS" => Ok(Protocol::Tls), - _ => Err(IceError::BadCandidate(format!("Unknown protocol: {}", s))), - } -} - -fn parse_candidate_kind(s: &str) -> Result { - match s { - "host" => Ok(CandidateKind::Host), - "srflx" => Ok(CandidateKind::ServerReflexive), - "prflx" => Ok(CandidateKind::PeerReflexive), - "relay" => Ok(CandidateKind::Relayed), - _ => Err(IceError::BadCandidate(format!( - "Unknown candidate type: {}", - s - ))), - } -} + let ip_addr = || { + not_sp().and_then(|s| { + s.parse::() + .map_err(StreamErrorFor::::message_format) + }) + }; -fn parse_tcptype(s: &str) -> Result { - match s { - "active" => Ok(TcpType::Active), - "passive" => Ok(TcpType::Passive), - "so" => Ok(TcpType::So), - _ => Err(IceError::BadCandidate(format!("Unknown tcptype: {}", s))), - } + let kind = choice(( + string("host").map(|_| CandidateKind::Host), + string("prflx").map(|_| CandidateKind::PeerReflexive), + string("srflx").map(|_| CandidateKind::ServerReflexive), + string("relay").map(|_| CandidateKind::Relayed), + )); + + ( + string("candidate:").and_then(|s| { + s.parse::() + .map_err(StreamErrorFor::::message_format) + }), + not_sp(), + token(' '), + not_sp().and_then(|s| { + s.parse::() + .map_err(StreamErrorFor::::message_format) + }), + token(' '), + not_sp().and_then(|s| { + s.as_str().try_into().map_err(|_| { + StreamErrorFor::::message_format(format!("invalid protocol: {}", s)) + }) + }), + token(' '), + not_sp().and_then(|s| { + s.parse::() + .map_err(StreamErrorFor::::message_format) + }), + token(' '), + ip_addr(), + token(' '), + port(), + string(" typ "), + kind, + optional(( + attempt(string(" raddr ")), + ip_addr(), + string(" rport "), + port(), + )), + optional(( + attempt(string(" tcptype ")), + not_sp().and_then(|s| { + TcpType::from_str(s.as_str()).map_err(StreamErrorFor::::message_format) + }), + )), + optional((attempt(string(" generation ")), not_sp())), + optional((attempt(string(" network-id ")), not_sp())), + optional((attempt(string(" ufrag ")), not_sp())), + optional((attempt(string(" network-cost ")), not_sp())), + ) + .map( + |( + _, + found, + _, + comp_id, + _, + proto, + _, + prio, + _, + addr, + _, + port, + _, + kind, + raddr, // (" raddr ", addr, " rport ", port) + tcptype, // (" tcptype ", tcptype) + _, // (" generation ", generation) + _, // (" network-id ", network_id) + ufrag, // (" ufrag ", ufrag) + _, // ("network-cost", network_cost) + )| { + Candidate::parsed( + found, + comp_id, + proto, + prio, // remote candidates calculate prio on their side + SocketAddr::from((addr, port)), + kind, + raddr.map(|(_, addr, _, port)| SocketAddr::from((addr, port))), + tcptype.map(|(_, tcptype)| tcptype), + ufrag.map(|(_, u)| u), + ) + }, + ) } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_host_candidate() { - let s = "candidate:1 1 UDP 2130706431 192.168.1.100 5000 typ host"; - let c = parse_candidate(s).unwrap(); - assert_eq!(c.kind(), CandidateKind::Host); - assert_eq!(c.addr().ip().to_string(), "192.168.1.100"); - assert_eq!(c.addr().port(), 5000); - assert_eq!(c.proto(), Protocol::Udp); - } - - #[test] - fn parse_srflx_candidate() { - let s = "candidate:2 1 UDP 1694498815 203.0.113.1 5001 typ srflx raddr 192.168.1.100 rport 5000"; - let c = parse_candidate(s).unwrap(); - assert_eq!(c.kind(), CandidateKind::ServerReflexive); - assert_eq!(c.addr().ip().to_string(), "203.0.113.1"); - assert_eq!(c.addr().port(), 5001); - } - - #[test] - fn parse_relay_candidate() { - let s = - "candidate:3 1 UDP 16777215 198.51.100.1 5002 typ relay raddr 192.168.1.100 rport 5000"; - let c = parse_candidate(s).unwrap(); - assert_eq!(c.kind(), CandidateKind::Relayed); - assert_eq!(c.addr().ip().to_string(), "198.51.100.1"); - assert_eq!(c.addr().port(), 5002); - } - - #[test] - fn parse_tcp_candidate() { - let s = "candidate:4 1 TCP 2128609279 192.168.1.100 9000 typ host tcptype active"; - let c = parse_candidate(s).unwrap(); - assert_eq!(c.kind(), CandidateKind::Host); - assert_eq!(c.proto(), Protocol::Tcp); - assert_eq!(c.tcptype(), Some(TcpType::Active)); - } - - #[test] - fn parse_without_prefix() { - let s = "1 1 UDP 2130706431 192.168.1.100 5000 typ host"; - let c = parse_candidate(s).unwrap(); - assert_eq!(c.kind(), CandidateKind::Host); - } - - #[test] - fn parse_ipv6_candidate() { - let s = "candidate:1 1 UDP 2130706431 2001:db8::1 5000 typ host"; - let c = parse_candidate(s).unwrap(); - assert_eq!(c.addr().ip().to_string(), "2001:db8::1"); - } - - #[test] - fn parse_invalid_too_short() { - let s = "candidate:1 1 UDP 2130706431"; - assert!(parse_candidate(s).is_err()); - } - - #[test] - fn parse_invalid_ip() { - let s = "candidate:1 1 UDP 2130706431 not-an-ip 5000 typ host"; - assert!(parse_candidate(s).is_err()); - } +fn not_sp() -> impl Parser +where + Input: Stream, + Input::Error: ParseError, +{ + many1(satisfy(|c| c != ' ' && c != '\r' && c != '\n')) } diff --git a/src/config.rs b/src/config.rs index 9292e78da..660654608 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,9 +4,9 @@ use std::time::{Duration, Instant}; use crate::config::DtlsCert; use crate::crypto::CryptoProvider; use crate::format::CodecConfig; -use crate::ice::IceCreds; use crate::rtp_::{Bitrate, Extension, ExtensionMap}; use crate::Rtc; +use str0m_ice::IceCreds; /// Customized config for creating an [`Rtc`] instance. /// diff --git a/src/sdp/parser.rs b/src/sdp/parser.rs index 62802f47e..c38a7fa79 100644 --- a/src/sdp/parser.rs +++ b/src/sdp/parser.rs @@ -4,14 +4,11 @@ use combine::parser::combinator::*; use combine::stream::StreamErrorFor; use combine::*; use combine::{ParseError, Parser, Stream}; -use std::net::{IpAddr, SocketAddr}; -use std::str::FromStr; +use str0m_ice::candidate; use crate::crypto::Fingerprint; -use crate::io::TcpType; use crate::rtp_::{Direction, Extension, Frequency, Mid, Pt, SessionId, Ssrc}; -use crate::sdp::SdpError; -use crate::{Candidate, CandidateKind}; +use crate::Candidate; use super::data::*; @@ -203,117 +200,6 @@ where )) } -/// Parser for candidate, without attribute prefix (a=). -fn candidate() -> impl Parser -where - Input: Stream, - Input::Error: ParseError, -{ - // Reference: https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 - let port = || { - not_sp::().and_then(|s| { - s.parse::() - .map_err(StreamErrorFor::::message_format) - }) - }; - - let ip_addr = || { - not_sp().and_then(|s| { - s.parse::() - .map_err(StreamErrorFor::::message_format) - }) - }; - - let kind = choice(( - string("host").map(|_| CandidateKind::Host), - string("prflx").map(|_| CandidateKind::PeerReflexive), - string("srflx").map(|_| CandidateKind::ServerReflexive), - string("relay").map(|_| CandidateKind::Relayed), - )); - - ( - string("candidate:").and_then(|s| { - s.parse::() - .map_err(StreamErrorFor::::message_format) - }), - not_sp(), - token(' '), - not_sp().and_then(|s| { - s.parse::() - .map_err(StreamErrorFor::::message_format) - }), - token(' '), - not_sp().and_then(|s| { - s.as_str().try_into().map_err(|_| { - StreamErrorFor::::message_format(format!("invalid protocol: {}", s)) - }) - }), - token(' '), - not_sp().and_then(|s| { - s.parse::() - .map_err(StreamErrorFor::::message_format) - }), - token(' '), - ip_addr(), - token(' '), - port(), - string(" typ "), - kind, - optional(( - attempt(string(" raddr ")), - ip_addr(), - string(" rport "), - port(), - )), - optional(( - attempt(string(" tcptype ")), - not_sp().and_then(|s| { - TcpType::from_str(s.as_str()).map_err(StreamErrorFor::::message_format) - }), - )), - optional((attempt(string(" generation ")), not_sp())), - optional((attempt(string(" network-id ")), not_sp())), - optional((attempt(string(" ufrag ")), not_sp())), - optional((attempt(string(" network-cost ")), not_sp())), - ) - .map( - |( - _, - found, - _, - comp_id, - _, - proto, - _, - prio, - _, - addr, - _, - port, - _, - kind, - raddr, // (" raddr ", addr, " rport ", port) - tcptype, // (" tcptype ", tcptype) - _, // (" generation ", generation) - _, // (" network-id ", network_id) - ufrag, // (" ufrag ", ufrag) - _, // ("network-cost", network_cost) - )| { - Candidate::parsed( - found, - comp_id, - proto, - prio, // remote candidates calculate prio on their side - SocketAddr::from((addr, port)), - kind, - raddr.map(|(_, addr, _, port)| SocketAddr::from((addr, port))), - tcptype.map(|(_, tcptype)| tcptype), - ufrag.map(|(_, u)| u), - ) - }, - ) -} - /// Parser for a=candidate lines. pub(crate) fn candidate_attribute() -> impl Parser where From 73d082ec7c67e7c1ca12b242878c6a58cef5a1ec Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 17:15:51 +1100 Subject: [PATCH 09/10] Revert unnecessary changes --- ice/src/agent.rs | 26 +++++++++++++------------- ice/src/lib.rs | 4 +--- ice/src/pair.rs | 3 ++- ice/src/sdp.rs | 4 ---- ice/src/stun.rs | 13 ++++++------- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/ice/src/agent.rs b/ice/src/agent.rs index ac4c946f8..b2635143f 100644 --- a/ice/src/agent.rs +++ b/ice/src/agent.rs @@ -8,12 +8,12 @@ use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; use tracing::{debug, trace, warn}; +use crate::id::Id; use crate::io::Transmit; use crate::io::{Protocol, StunPacket}; use crate::io::{StunMessage, TransId}; use crate::preference::default_local_preference; -use crate::stun::{Id, StunTiming}; -use crate::{StunClass, StunMethod}; +use crate::stun::{Class, Method, StunTiming}; use str0m_proto::crypto::Sha1HmacProvider; use str0m_proto::{NonCryptographicRng, Pii, DATAGRAM_MTU, DATAGRAM_MTU_WARN}; @@ -973,13 +973,13 @@ impl IceAgent { let method = message.method(); let class = message.class(); match (method, class) { - (StunMethod::Binding, StunClass::Indication) => { + (Method::Binding, Class::Indication) => { // https://datatracker.ietf.org/doc/html/rfc8489#section-6.3.2 // An Indication can be safely ignored, its purpose is to refresh NATs in the // network path. Some clients MAY omit USERNAME attribute. false } - (StunMethod::Binding, StunClass::Request) => { + (Method::Binding, Class::Request) => { // The username for the credential is formed by concatenating the // username fragment provided by the peer with the username fragment of // the ICE agent sending the request, separated by a colon (":"). @@ -1009,7 +1009,7 @@ impl IceAgent { do_integrity_check(true) } - (StunMethod::Binding, StunClass::Success | StunClass::Failure) => { + (Method::Binding, Class::Success | Class::Failure) => { let belongs_to_a_candidate_pair = self .candidate_pairs .iter() @@ -1022,23 +1022,23 @@ impl IceAgent { do_integrity_check(false) } - (StunMethod::Binding, StunClass::Unknown) => { + (Method::Binding, Class::Unknown) => { // Without a known class, it's impossible to know how to validate the message trace!("Message rejected, unknown STUN class"); false } - (StunMethod::Unknown, _) => { + (Method::Unknown, _) => { // Without a known method, it's impossible to know how to validate the message trace!("Message rejected, unknown STUN method"); false } ( - StunMethod::Allocate - | StunMethod::Refresh - | StunMethod::Send - | StunMethod::Data - | StunMethod::CreatePermission - | StunMethod::ChannelBind, + Method::Allocate + | Method::Refresh + | Method::Send + | Method::Data + | Method::CreatePermission + | Method::ChannelBind, _, ) => { // Unexpected TURN related message diff --git a/ice/src/lib.rs b/ice/src/lib.rs index f05592210..ffee2248b 100644 --- a/ice/src/lib.rs +++ b/ice/src/lib.rs @@ -60,9 +60,7 @@ pub use candidate::{Candidate, CandidateBuilder, CandidateKind}; pub use error::{IceError, NetError, StunError}; pub use io::StunPacket; pub use preference::default_local_preference; -pub use stun::{ - Class as StunClass, Method as StunMethod, StunMessage, StunMessageBuilder, TransId, -}; +pub use stun::{StunMessage, StunMessageBuilder, TransId}; #[doc(hidden)] pub use sdp::candidate; diff --git a/ice/src/pair.rs b/ice/src/pair.rs index 43881339c..db8d76087 100644 --- a/ice/src/pair.rs +++ b/ice/src/pair.rs @@ -4,7 +4,8 @@ use std::time::{Duration, Instant}; use tracing::{debug, trace}; -use crate::stun::{Id, StunTiming, TransId, DEFAULT_MAX_RETRANSMITS}; +use crate::id::Id; +use crate::stun::{StunTiming, TransId, DEFAULT_MAX_RETRANSMITS}; use crate::Candidate; use str0m_proto::Pii; diff --git a/ice/src/sdp.rs b/ice/src/sdp.rs index deea04554..5298cb98c 100644 --- a/ice/src/sdp.rs +++ b/ice/src/sdp.rs @@ -1,7 +1,3 @@ -//! Simple SDP candidate parser for ICE -//! -//! This module provides basic SDP candidate parsing functionality needed by the ICE implementation. - use std::net::{IpAddr, SocketAddr}; use std::str::FromStr as _; diff --git a/ice/src/stun.rs b/ice/src/stun.rs index ae2bf8d2c..9be4e556a 100644 --- a/ice/src/stun.rs +++ b/ice/src/stun.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; use tracing::warn; -pub const DEFAULT_MAX_RETRANSMITS: usize = 9; +pub(crate) const DEFAULT_MAX_RETRANSMITS: usize = 9; #[derive(Debug)] // Purposely not `Clone` / `Copy` to ensure we always use the latest one everywhere. pub struct StunTiming { @@ -71,6 +71,8 @@ impl Default for StunTiming { } } +use crate::StunError; + /// STUN transaction ID. #[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct TransId([u8; 12]); @@ -114,7 +116,7 @@ pub struct StunMessage<'a> { impl<'a> StunMessage<'a> { /// Parse a STUN message from a slice of bytes. - pub fn parse(buf: &'a [u8]) -> Result, StunError> { + pub fn parse(buf: &[u8]) -> Result { if buf.len() < 4 { return Err(StunError::Parse("Buffer too short".into())); } @@ -472,7 +474,7 @@ impl<'a> StunMessage<'a> { const MAGIC: &[u8] = &[0x21, 0x12, 0xA4, 0x42]; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub enum Class { +pub(crate) enum Class { Request, Indication, Success, @@ -505,7 +507,7 @@ impl Class { } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub enum Method { +pub(crate) enum Method { Binding, // TURN specific Allocate, @@ -656,11 +658,8 @@ impl<'a> Attributes<'a> { use std::{io, str}; -use crate::StunError; use str0m_proto::NonCryptographicRng; -pub use crate::id::Id; - const PAD: [u8; 4] = [0, 0, 0, 0]; impl<'a> Attributes<'a> { const ALTERNATE_SERVER: u16 = 0x8023; From 8c1a0ed1964922d91fb101f9771c9926de370c7f Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Feb 2026 17:23:45 +1100 Subject: [PATCH 10/10] More cleanup --- ice/src/agent.rs | 3 +-- ice/src/lib.rs | 1 - ice/src/pair.rs | 3 +-- {ice => proto}/src/id.rs | 2 +- proto/src/lib.rs | 5 +++- proto/src/net.rs | 39 +++++++++++++++++++-------- src/change/sdp.rs | 3 ++- src/dtls.rs | 3 ++- src/io/mod.rs | 55 +-------------------------------------- src/lib.rs | 3 ++- src/media/mod.rs | 3 ++- src/rtp/id.rs | 3 +-- src/sdp/data.rs | 2 +- src/sdp/parser.rs | 1 + src/session.rs | 4 ++- src/util/mod.rs | 5 ---- tests/handshake-direct.rs | 2 +- 17 files changed, 51 insertions(+), 86 deletions(-) rename {ice => proto}/src/id.rs (95%) diff --git a/ice/src/agent.rs b/ice/src/agent.rs index b2635143f..21360d377 100644 --- a/ice/src/agent.rs +++ b/ice/src/agent.rs @@ -8,7 +8,6 @@ use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; use tracing::{debug, trace, warn}; -use crate::id::Id; use crate::io::Transmit; use crate::io::{Protocol, StunPacket}; use crate::io::{StunMessage, TransId}; @@ -16,7 +15,7 @@ use crate::preference::default_local_preference; use crate::stun::{Class, Method, StunTiming}; use str0m_proto::crypto::Sha1HmacProvider; -use str0m_proto::{NonCryptographicRng, Pii, DATAGRAM_MTU, DATAGRAM_MTU_WARN}; +use str0m_proto::{Id, NonCryptographicRng, Pii, DATAGRAM_MTU, DATAGRAM_MTU_WARN}; use crate::candidate::{Candidate, CandidateKind}; use crate::pair::{CandidatePair, CheckState, PairId}; diff --git a/ice/src/lib.rs b/ice/src/lib.rs index ffee2248b..af5d22e12 100644 --- a/ice/src/lib.rs +++ b/ice/src/lib.rs @@ -39,7 +39,6 @@ mod agent; mod candidate; mod error; -mod id; mod io; mod pair; mod preference; diff --git a/ice/src/pair.rs b/ice/src/pair.rs index db8d76087..7a836170b 100644 --- a/ice/src/pair.rs +++ b/ice/src/pair.rs @@ -4,10 +4,9 @@ use std::time::{Duration, Instant}; use tracing::{debug, trace}; -use crate::id::Id; use crate::stun::{StunTiming, TransId, DEFAULT_MAX_RETRANSMITS}; use crate::Candidate; -use str0m_proto::Pii; +use str0m_proto::{Id, Pii}; use crate::candidate::CandidateKind; diff --git a/ice/src/id.rs b/proto/src/id.rs similarity index 95% rename from ice/src/id.rs rename to proto/src/id.rs index 53aa4e1d1..8ab6ee266 100644 --- a/ice/src/id.rs +++ b/proto/src/id.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::from_utf8; -use str0m_proto::NonCryptographicRng; +use crate::NonCryptographicRng; // deliberate subset of ice-char, etc that are "safe" const CHARS: &[u8] = b"abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789"; diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 7c2475c2d..b6b12f747 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -12,7 +12,10 @@ pub use bandwidth::{Bitrate, DataSize}; pub mod crypto; pub mod net; -pub use net::{Protocol, TcpType, Transmit}; +pub use net::{DatagramSend, Protocol, TcpType, Transmit}; pub mod util; pub use util::{NonCryptographicRng, Pii}; + +mod id; +pub use id::Id; diff --git a/proto/src/net.rs b/proto/src/net.rs index 5ce5a539d..25a7ae349 100644 --- a/proto/src/net.rs +++ b/proto/src/net.rs @@ -1,8 +1,8 @@ //! Network types shared across str0m crates. -use std::fmt; use std::net::SocketAddr; use std::str::FromStr; +use std::{fmt, ops::Deref}; use serde::{Deserialize, Serialize}; @@ -123,23 +123,16 @@ impl FromStr for TcpType { } /// An instruction to send an outgoing packet. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct Transmit { /// Protocol the transmission should use. pub proto: Protocol, - /// The source IP this packet should be sent from. - /// - /// For ICE it's important to send outgoing packets from the correct IP address. - /// The IP could come from a local socket or relayed over a TURN server. Features like - /// hole-punching will only work if the packets are routed through the correct interfaces. pub source: SocketAddr, - /// The destination address this datagram should be sent to. pub destination: SocketAddr, - /// Contents of the datagram. - pub contents: Vec, + pub contents: DatagramSend, } impl fmt::Debug for Transmit { @@ -148,7 +141,31 @@ impl fmt::Debug for Transmit { .field("proto", &self.proto) .field("source", &self.source) .field("destination", &self.destination) - .field("contents_len", &self.contents.len()) + .field("len", &self.contents.len()) .finish() } } + +/// A wrapper for some payload that is to be sent. +#[derive(Debug, Serialize, Deserialize)] +pub struct DatagramSend(Vec); + +impl From> for DatagramSend { + fn from(value: Vec) -> Self { + DatagramSend(value) + } +} + +impl From for Vec { + fn from(value: DatagramSend) -> Self { + value.0 + } +} + +impl Deref for DatagramSend { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/change/sdp.rs b/src/change/sdp.rs index 5949161d4..64084d441 100644 --- a/src/change/sdp.rs +++ b/src/change/sdp.rs @@ -3,11 +3,12 @@ use std::fmt; use std::ops::{Deref, DerefMut}; +use str0m_proto::Id; + use crate::channel::ChannelId; use crate::crypto::Fingerprint; use crate::format::CodecConfig; use crate::format::PayloadParams; -use crate::io::Id; use crate::media::{Media, Rids, Simulcast}; use crate::packet::MediaKind; use crate::rtp_::MidRid; diff --git a/src/dtls.rs b/src/dtls.rs index b34801c76..4e4b93481 100644 --- a/src/dtls.rs +++ b/src/dtls.rs @@ -3,12 +3,13 @@ use std::io; use std::panic::{RefUnwindSafe, UnwindSafe}; use std::time::Instant; +use str0m_proto::DatagramSend; + use crate::crypto::dtls::{DtlsCert, DtlsOutput}; use crate::crypto::dtls::{DtlsInstance, DtlsProvider}; use crate::crypto::Fingerprint; use crate::crypto::Sha256Provider; use crate::crypto::{CryptoError, DtlsError}; -use crate::io::DatagramSend; use crate::util::already_happened; /// Encapsulation of DTLS. diff --git a/src/io/mod.rs b/src/io/mod.rs index 3d7434fe9..bc63a8322 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -8,6 +8,7 @@ use std::net::SocketAddr; use serde::{Deserialize, Serialize}; +use str0m_ice::Transmit; // Re-export types from str0m-ice pub use str0m_ice::{NetError, StunMessage, StunPacket}; @@ -32,8 +33,6 @@ pub struct Receive<'a> { pub contents: DatagramRecv<'a>, } -pub(crate) use str0m_ice::stun::Id; - pub use str0m_proto::DATAGRAM_MTU; pub use str0m_proto::DATAGRAM_MTU_WARN; @@ -45,58 +44,6 @@ pub(crate) const DATAGRAM_MAX_PACKET_SIZE: usize = 2000; #[allow(dead_code)] pub const MAX_RTP_OVERHEAD: usize = 80; -use std::ops::Deref; - -/// An instruction to send an outgoing packet. -/// -/// This wraps the proto Transmit type but uses DatagramSend for contents. -#[derive(Serialize, Deserialize)] -pub struct Transmit { - /// Protocol the transmission should use. - pub proto: Protocol, - /// The source IP this packet should be sent from. - pub source: SocketAddr, - /// The destination address this datagram should be sent to. - pub destination: SocketAddr, - /// Contents of the datagram. - pub contents: DatagramSend, -} - -impl fmt::Debug for Transmit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Transmit") - .field("proto", &self.proto) - .field("source", &self.source) - .field("destination", &self.destination) - .field("len", &self.contents.len()) - .finish() - } -} - -/// A wrapper for some payload that is to be sent. -#[derive(Debug, Serialize, Deserialize)] -pub struct DatagramSend(Vec); - -impl From> for DatagramSend { - fn from(value: Vec) -> Self { - DatagramSend(value) - } -} - -impl From for Vec { - fn from(value: DatagramSend) -> Self { - value.0 - } -} - -impl Deref for DatagramSend { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - impl<'a> Receive<'a> { /// Creates a new instance by trying to parse the contents of `buf`. pub fn new( diff --git a/src/lib.rs b/src/lib.rs index 8fd3a45d7..dc30e3af7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -771,7 +771,8 @@ pub mod error; /// Network related types to get socket data in/out of [`Rtc`]. pub mod net { - pub use crate::io::{DatagramRecv, DatagramSend, Protocol, Receive, TcpType, Transmit}; + pub use crate::io::{DatagramRecv, Protocol, Receive, TcpType}; + pub use str0m_proto::{DatagramSend, Transmit}; } const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/media/mod.rs b/src/media/mod.rs index 7a51741a1..e671179c9 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -5,7 +5,7 @@ use std::time::Instant; use crate::change::AddMedia; use crate::format::CodecConfig; -use crate::io::{Id, DATAGRAM_MTU}; +use crate::io::DATAGRAM_MTU; use crate::packet::{DepacketizingBuffer, Payloader, RtpMeta}; use crate::rtp_::ExtensionMap; use crate::rtp_::MidRid; @@ -23,6 +23,7 @@ mod event; pub use event::*; mod writer; +use str0m_proto::Id; pub use writer::Writer; pub use crate::packet::MediaKind; diff --git a/src/rtp/id.rs b/src/rtp/id.rs index fcb839018..f475a33b1 100644 --- a/src/rtp/id.rs +++ b/src/rtp/id.rs @@ -4,7 +4,6 @@ use std::str::from_utf8; use serde::{Deserialize, Serialize}; -use crate::io::Id; use crate::util::NonCryptographicRng; macro_rules! str_id { @@ -12,7 +11,7 @@ macro_rules! str_id { impl $id { /// Creates a new random id. pub fn new() -> $id { - let mut arr = Id::<$num>::random().into_array(); + let mut arr = str0m_proto::Id::<$num>::random().into_array(); for i in $new_len..$num { arr[i] = b' '; } diff --git a/src/sdp/data.rs b/src/sdp/data.rs index dfd769645..a2a53de4f 100644 --- a/src/sdp/data.rs +++ b/src/sdp/data.rs @@ -5,13 +5,13 @@ use std::collections::HashMap; use std::collections::HashSet; use std::fmt::{self}; use std::ops::Deref; +use str0m_proto::Id; use crate::crypto::Fingerprint; use crate::format::Codec; use crate::format::CodecSpec; use crate::format::FormatParams; use crate::format::PayloadParams; -use crate::io::Id; use crate::packet::H265ProfileTierLevel; use crate::rtp_::{Direction, Extension, Frequency, Mid, Pt, Rid, SessionId, Ssrc}; use crate::{Candidate, IceCreds, VERSION}; diff --git a/src/sdp/parser.rs b/src/sdp/parser.rs index c38a7fa79..f6cd0d5ca 100644 --- a/src/sdp/parser.rs +++ b/src/sdp/parser.rs @@ -746,6 +746,7 @@ where mod test { use super::*; use crate::io::Protocol; + use str0m_ice::TcpType; #[test] fn line_a() { diff --git a/src/session.rs b/src/session.rs index 8f5f47b09..934f1dcd7 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,6 +1,8 @@ use std::collections::{HashMap, VecDeque}; use std::time::{Duration, Instant}; +use str0m_proto::DatagramSend; + use crate::bwe::BweKind; use crate::bwe_::Bwe; use crate::config::KeyingMaterial; @@ -8,7 +10,7 @@ use crate::crypto::dtls::SrtpProfile; use crate::crypto::CryptoProvider; use crate::format::CodecConfig; use crate::format::PayloadParams; -use crate::io::{DatagramSend, DATAGRAM_MTU, DATAGRAM_MTU_WARN}; +use crate::io::{DATAGRAM_MTU, DATAGRAM_MTU_WARN}; use crate::media::Media; use crate::media::{KeyframeRequestKind, MID_PROBE}; use crate::media::{MediaAdded, MediaChanged}; diff --git a/src/util/mod.rs b/src/util/mod.rs index 639d5a644..6162f29d4 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -112,9 +112,4 @@ impl NonCryptographicRng { pub fn u64() -> u64 { fastrand::u64(..) } - - #[inline(always)] - pub fn f32() -> f32 { - fastrand::f32() - } } diff --git a/tests/handshake-direct.rs b/tests/handshake-direct.rs index d9d4044a1..e819aea7f 100644 --- a/tests/handshake-direct.rs +++ b/tests/handshake-direct.rs @@ -5,8 +5,8 @@ use std::time::{Duration, Instant}; use str0m::channel::{ChannelConfig, ChannelId, Reliability}; use str0m::config::Fingerprint; -use str0m::ice::IceCreds; use str0m::net::{Protocol, Receive}; +use str0m::IceCreds; use str0m::{Candidate, Event, IceConnectionState, Input, Output, Rtc, RtcConfig, RtcError}; use tracing::{info_span, Span};