#![allow(dead_code)]
use std::collections::HashMap;
use std::net::SocketAddr;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CandidateType {
Host,
ServerReflexive,
Relay,
PeerReflexive,
}
impl CandidateType {
#[must_use]
pub const fn type_preference(&self) -> u32 {
match self {
Self::Host => 126,
Self::PeerReflexive => 110,
Self::ServerReflexive => 100,
Self::Relay => 0,
}
}
#[must_use]
pub const fn sdp_name(&self) -> &'static str {
match self {
Self::Host => "host",
Self::ServerReflexive => "srflx",
Self::Relay => "relay",
Self::PeerReflexive => "prflx",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransportProtocol {
Udp,
Tcp,
}
impl TransportProtocol {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Udp => "UDP",
Self::Tcp => "TCP",
}
}
}
#[derive(Debug, Clone)]
pub struct IceCandidate {
pub foundation: String,
pub component: u8,
pub protocol: TransportProtocol,
pub priority: u32,
pub address: SocketAddr,
pub candidate_type: CandidateType,
pub related_address: Option<SocketAddr>,
pub generation: u32,
}
impl IceCandidate {
#[must_use]
pub fn host(address: SocketAddr, component: u8) -> Self {
let priority = Self::compute_priority(CandidateType::Host, 65535, component);
Self {
foundation: format!("host-{}-{}", address.ip(), address.port()),
component,
protocol: TransportProtocol::Udp,
priority,
address,
candidate_type: CandidateType::Host,
related_address: None,
generation: 0,
}
}
#[must_use]
pub fn server_reflexive(srflx_addr: SocketAddr, base_addr: SocketAddr, component: u8) -> Self {
let priority = Self::compute_priority(CandidateType::ServerReflexive, 65535, component);
Self {
foundation: format!("srflx-{}-{}", srflx_addr.ip(), base_addr.port()),
component,
protocol: TransportProtocol::Udp,
priority,
address: srflx_addr,
candidate_type: CandidateType::ServerReflexive,
related_address: Some(base_addr),
generation: 0,
}
}
#[must_use]
pub fn relay(relay_addr: SocketAddr, base_addr: SocketAddr, component: u8) -> Self {
let priority = Self::compute_priority(CandidateType::Relay, 65535, component);
Self {
foundation: format!("relay-{}-{}", relay_addr.ip(), relay_addr.port()),
component,
protocol: TransportProtocol::Udp,
priority,
address: relay_addr,
candidate_type: CandidateType::Relay,
related_address: Some(base_addr),
generation: 0,
}
}
#[must_use]
pub fn compute_priority(candidate_type: CandidateType, local_pref: u32, component: u8) -> u32 {
let type_pref = candidate_type.type_preference();
(2u32.pow(24)) * type_pref
+ (2u32.pow(8)) * local_pref
+ (256u32.saturating_sub(u32::from(component)))
}
#[must_use]
pub fn to_sdp(&self) -> String {
let mut sdp = format!(
"candidate:{} {} {} {} {} {} typ {}",
self.foundation,
self.component,
self.protocol.name().to_lowercase(),
self.priority,
self.address.ip(),
self.address.port(),
self.candidate_type.sdp_name(),
);
if let Some(rel) = self.related_address {
sdp.push_str(&format!(" raddr {} rport {}", rel.ip(), rel.port()));
}
sdp.push_str(&format!(" generation {}", self.generation));
sdp
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairState {
Waiting,
InProgress,
Succeeded,
Failed,
Frozen,
}
#[derive(Debug)]
pub struct CandidatePair {
pub local: IceCandidate,
pub remote: IceCandidate,
pub priority: u64,
pub state: PairState,
pub nominated: bool,
pub check_count: u32,
pub last_check: Option<Instant>,
pub rtt: Option<Duration>,
}
impl CandidatePair {
#[must_use]
pub fn new(local: IceCandidate, remote: IceCandidate, controlling: bool) -> Self {
let g = if controlling {
u64::from(local.priority)
} else {
u64::from(remote.priority)
};
let d = if controlling {
u64::from(remote.priority)
} else {
u64::from(local.priority)
};
let priority = 2u64.pow(32) * g.min(d) + 2 * g.max(d) + u64::from(g > d);
Self {
local,
remote,
priority,
state: PairState::Frozen,
nominated: false,
check_count: 0,
last_check: None,
rtt: None,
}
}
#[must_use]
pub const fn can_check(&self) -> bool {
matches!(self.state, PairState::Waiting)
}
#[must_use]
pub fn key(&self) -> String {
format!("{}-{}", self.local.address, self.remote.address)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IceRole {
Controlling,
Controlled,
}
impl IceRole {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Controlling => "controlling",
Self::Controlled => "controlled",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IceState {
New,
Gathering,
Checking,
Connected,
Completed,
Failed,
Closed,
}
#[derive(Debug, Clone)]
pub struct IceConfig {
pub role: IceRole,
pub stun_servers: Vec<SocketAddr>,
pub turn_server: Option<SocketAddr>,
pub turn_username: Option<String>,
pub turn_password: Option<String>,
pub check_interval: Duration,
pub max_checks: u32,
pub gather_timeout: Duration,
pub trickle: bool,
}
impl Default for IceConfig {
fn default() -> Self {
Self {
role: IceRole::Controlling,
stun_servers: Vec::new(),
turn_server: None,
turn_username: None,
turn_password: None,
check_interval: Duration::from_millis(20),
max_checks: 6,
gather_timeout: Duration::from_secs(5),
trickle: true,
}
}
}
impl IceConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_stun(mut self, addr: SocketAddr) -> Self {
self.stun_servers.push(addr);
self
}
#[must_use]
pub fn with_turn(mut self, addr: SocketAddr, username: &str, password: &str) -> Self {
self.turn_server = Some(addr);
self.turn_username = Some(username.to_owned());
self.turn_password = Some(password.to_owned());
self
}
#[must_use]
pub const fn with_role(mut self, role: IceRole) -> Self {
self.role = role;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckResult {
Success,
Timeout,
Unreachable,
}
pub struct IceAgent {
config: IceConfig,
state: IceState,
local_candidates: Vec<IceCandidate>,
remote_candidates: Vec<IceCandidate>,
pairs: Vec<CandidatePair>,
nominated_pair_key: Option<String>,
local_ufrag: String,
local_pwd: String,
tie_breaker: u64,
}
impl IceAgent {
#[must_use]
pub fn new(config: IceConfig) -> Self {
let tie_breaker = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(12345);
Self {
config,
state: IceState::New,
local_candidates: Vec::new(),
remote_candidates: Vec::new(),
pairs: Vec::new(),
nominated_pair_key: None,
local_ufrag: format!("ufrag{tie_breaker:08x}"),
local_pwd: format!("pwd{tie_breaker:016x}"),
tie_breaker,
}
}
#[must_use]
pub const fn state(&self) -> IceState {
self.state
}
#[must_use]
pub const fn role(&self) -> IceRole {
self.config.role
}
#[must_use]
pub fn local_ufrag(&self) -> &str {
&self.local_ufrag
}
#[must_use]
pub fn local_pwd(&self) -> &str {
&self.local_pwd
}
#[must_use]
pub fn local_candidates(&self) -> &[IceCandidate] {
&self.local_candidates
}
#[must_use]
pub fn remote_candidates(&self) -> &[IceCandidate] {
&self.remote_candidates
}
#[must_use]
pub fn candidate_pairs(&self) -> &[CandidatePair] {
&self.pairs
}
#[must_use]
pub fn pairs_in_state(&self, state: PairState) -> usize {
self.pairs.iter().filter(|p| p.state == state).count()
}
pub fn start_gathering(&mut self) {
self.state = IceState::Gathering;
}
pub fn add_local_candidate(&mut self, candidate: IceCandidate) {
self.local_candidates.push(candidate);
self.pair_new_local(self.local_candidates.len() - 1);
}
pub fn add_remote_candidate(&mut self, candidate: IceCandidate) {
self.remote_candidates.push(candidate);
let remote_idx = self.remote_candidates.len() - 1;
self.pair_new_remote(remote_idx);
}
pub fn start_checks(&mut self) {
self.state = IceState::Checking;
let mut seen_foundations: HashMap<String, bool> = HashMap::new();
for pair in &mut self.pairs {
let foundation = pair.local.foundation.clone();
if let std::collections::hash_map::Entry::Vacant(e) = seen_foundations.entry(foundation)
{
pair.state = PairState::Waiting;
e.insert(true);
}
}
}
pub fn perform_next_check(&mut self, result: CheckResult) -> Option<String> {
let idx = self
.pairs
.iter()
.position(|p| p.state == PairState::Waiting)?;
self.pairs[idx].state = PairState::InProgress;
self.pairs[idx].last_check = Some(Instant::now());
self.pairs[idx].check_count += 1;
let key = self.pairs[idx].key();
match result {
CheckResult::Success => {
self.pairs[idx].state = PairState::Succeeded;
self.pairs[idx].rtt = Some(Duration::from_millis(5));
if self.state == IceState::Checking {
self.state = IceState::Connected;
}
}
CheckResult::Timeout | CheckResult::Unreachable => {
self.pairs[idx].state = PairState::Failed;
if self.pairs.iter().all(|p| p.state == PairState::Failed) {
self.state = IceState::Failed;
}
}
}
Some(key)
}
pub fn nominate_best_pair(&mut self) -> Option<String> {
if self.config.role != IceRole::Controlling {
return None;
}
let best_idx = self
.pairs
.iter()
.enumerate()
.filter(|(_, p)| p.state == PairState::Succeeded)
.max_by_key(|(_, p)| p.priority)
.map(|(i, _)| i)?;
self.pairs[best_idx].nominated = true;
let key = self.pairs[best_idx].key();
self.nominated_pair_key = Some(key.clone());
self.state = IceState::Completed;
Some(key)
}
#[must_use]
pub fn nominated_pair(&self) -> Option<&CandidatePair> {
let key = self.nominated_pair_key.as_ref()?;
self.pairs.iter().find(|p| &p.key() == key)
}
pub fn close(&mut self) {
self.state = IceState::Closed;
}
fn pair_new_local(&mut self, local_idx: usize) {
let controlling = self.config.role == IceRole::Controlling;
let local = self.local_candidates[local_idx].clone();
let remote_count = self.remote_candidates.len();
for ri in 0..remote_count {
let remote = self.remote_candidates[ri].clone();
if local.component == remote.component {
let pair = CandidatePair::new(local.clone(), remote, controlling);
self.insert_pair(pair);
}
}
}
fn pair_new_remote(&mut self, remote_idx: usize) {
let controlling = self.config.role == IceRole::Controlling;
let remote = self.remote_candidates[remote_idx].clone();
let local_count = self.local_candidates.len();
for li in 0..local_count {
let local = self.local_candidates[li].clone();
if local.component == remote.component {
let pair = CandidatePair::new(local, remote.clone(), controlling);
self.insert_pair(pair);
}
}
}
fn insert_pair(&mut self, pair: CandidatePair) {
let pos = self
.pairs
.iter()
.position(|p| p.priority < pair.priority)
.unwrap_or(self.pairs.len());
self.pairs.insert(pos, pair);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn local_addr() -> SocketAddr {
"127.0.0.1:5000".parse().expect("valid addr")
}
fn remote_addr() -> SocketAddr {
"192.168.1.100:5000".parse().expect("valid addr")
}
#[test]
fn test_candidate_type_preference() {
assert_eq!(CandidateType::Host.type_preference(), 126);
assert!(CandidateType::Host.type_preference() > CandidateType::Relay.type_preference());
}
#[test]
fn test_candidate_type_sdp_names() {
assert_eq!(CandidateType::Host.sdp_name(), "host");
assert_eq!(CandidateType::ServerReflexive.sdp_name(), "srflx");
assert_eq!(CandidateType::Relay.sdp_name(), "relay");
}
#[test]
fn test_host_candidate_construction() {
let c = IceCandidate::host(local_addr(), 1);
assert_eq!(c.candidate_type, CandidateType::Host);
assert_eq!(c.component, 1);
assert_eq!(c.address, local_addr());
assert!(c.priority > 0);
}
#[test]
fn test_server_reflexive_candidate() {
let srflx: SocketAddr = "203.0.113.5:5000".parse().expect("valid addr");
let c = IceCandidate::server_reflexive(srflx, local_addr(), 1);
assert_eq!(c.candidate_type, CandidateType::ServerReflexive);
assert_eq!(c.related_address, Some(local_addr()));
}
#[test]
fn test_relay_candidate() {
let relay: SocketAddr = "198.51.100.10:3478".parse().expect("valid addr");
let c = IceCandidate::relay(relay, local_addr(), 1);
assert_eq!(c.candidate_type, CandidateType::Relay);
}
#[test]
fn test_candidate_priority_ordering() {
let host = IceCandidate::host(local_addr(), 1);
let srflx = IceCandidate::server_reflexive(remote_addr(), local_addr(), 1);
let relay = IceCandidate::relay(remote_addr(), local_addr(), 1);
assert!(host.priority > srflx.priority);
assert!(srflx.priority > relay.priority);
}
#[test]
fn test_candidate_to_sdp() {
let c = IceCandidate::host(local_addr(), 1);
let sdp = c.to_sdp();
assert!(sdp.starts_with("candidate:"));
assert!(sdp.contains("host"));
assert!(sdp.contains("udp"));
}
#[test]
fn test_ice_config_defaults() {
let cfg = IceConfig::default();
assert_eq!(cfg.role, IceRole::Controlling);
assert!(cfg.trickle);
assert!(cfg.stun_servers.is_empty());
}
#[test]
fn test_ice_config_builder() {
let stun: SocketAddr = "8.8.8.8:3478".parse().expect("valid addr");
let cfg = IceConfig::new()
.with_stun(stun)
.with_role(IceRole::Controlled);
assert_eq!(cfg.role, IceRole::Controlled);
assert_eq!(cfg.stun_servers.len(), 1);
}
#[test]
fn test_ice_agent_initial_state() {
let agent = IceAgent::new(IceConfig::default());
assert_eq!(agent.state(), IceState::New);
assert!(agent.local_candidates().is_empty());
assert!(agent.remote_candidates().is_empty());
}
#[test]
fn test_ice_agent_start_gathering() {
let mut agent = IceAgent::new(IceConfig::default());
agent.start_gathering();
assert_eq!(agent.state(), IceState::Gathering);
}
#[test]
fn test_ice_agent_pair_creation() {
let mut agent = IceAgent::new(IceConfig::default());
agent.add_local_candidate(IceCandidate::host(local_addr(), 1));
agent.add_remote_candidate(IceCandidate::host(remote_addr(), 1));
assert_eq!(agent.candidate_pairs().len(), 1);
}
#[test]
fn test_ice_agent_start_checks() {
let mut agent = IceAgent::new(IceConfig::default());
agent.add_local_candidate(IceCandidate::host(local_addr(), 1));
agent.add_remote_candidate(IceCandidate::host(remote_addr(), 1));
agent.start_checks();
assert_eq!(agent.pairs_in_state(PairState::Waiting), 1);
}
#[test]
fn test_ice_agent_check_success() {
let mut agent = IceAgent::new(IceConfig::default());
agent.add_local_candidate(IceCandidate::host(local_addr(), 1));
agent.add_remote_candidate(IceCandidate::host(remote_addr(), 1));
agent.start_checks();
agent.perform_next_check(CheckResult::Success);
assert_eq!(agent.state(), IceState::Connected);
}
#[test]
fn test_ice_agent_check_failure() {
let mut agent = IceAgent::new(IceConfig::default());
agent.add_local_candidate(IceCandidate::host(local_addr(), 1));
agent.add_remote_candidate(IceCandidate::host(remote_addr(), 1));
agent.start_checks();
agent.perform_next_check(CheckResult::Timeout);
assert_eq!(agent.pairs_in_state(PairState::Failed), 1);
assert_eq!(agent.state(), IceState::Failed);
}
#[test]
fn test_ice_agent_nomination() {
let mut agent = IceAgent::new(IceConfig::new().with_role(IceRole::Controlling));
agent.add_local_candidate(IceCandidate::host(local_addr(), 1));
agent.add_remote_candidate(IceCandidate::host(remote_addr(), 1));
agent.start_checks();
agent.perform_next_check(CheckResult::Success);
let key = agent.nominate_best_pair();
assert!(key.is_some());
assert_eq!(agent.state(), IceState::Completed);
assert!(agent.nominated_pair().is_some());
}
#[test]
fn test_ice_agent_controlled_cannot_nominate() {
let mut agent = IceAgent::new(IceConfig::new().with_role(IceRole::Controlled));
agent.add_local_candidate(IceCandidate::host(local_addr(), 1));
agent.add_remote_candidate(IceCandidate::host(remote_addr(), 1));
agent.start_checks();
agent.perform_next_check(CheckResult::Success);
assert!(agent.nominate_best_pair().is_none());
}
#[test]
fn test_ice_agent_close() {
let mut agent = IceAgent::new(IceConfig::default());
agent.close();
assert_eq!(agent.state(), IceState::Closed);
}
#[test]
fn test_ice_agent_credentials() {
let agent = IceAgent::new(IceConfig::default());
assert!(!agent.local_ufrag().is_empty());
assert!(!agent.local_pwd().is_empty());
}
#[test]
fn test_ice_role_names() {
assert_eq!(IceRole::Controlling.name(), "controlling");
assert_eq!(IceRole::Controlled.name(), "controlled");
}
#[test]
fn test_ice_different_components_dont_pair() {
let mut agent = IceAgent::new(IceConfig::default());
agent.add_local_candidate(IceCandidate::host(local_addr(), 1)); agent.add_remote_candidate(IceCandidate::host(remote_addr(), 2)); assert_eq!(agent.candidate_pairs().len(), 0);
}
}