use std::str::FromStr;
use std::time::Instant;
use stun::message::*;
use stun::textattrs::Username;
use super::*;
use crate::attributes::{
control::AttrControlling, priority::PriorityAttr, use_candidate::UseCandidateAttr,
};
use crate::candidate::candidate_host::*;
use crate::candidate::candidate_peer_reflexive::*;
use crate::candidate::candidate_relay::CandidateRelayConfig;
use crate::candidate::candidate_server_reflexive::*;
use crate::candidate::*;
fn test_crypto_provider() -> std::sync::Arc<dyn crypto::RTCCryptoProvider> {
crypto::default_provider().expect("a built-in crypto provider must be enabled for tests")
}
#[test]
fn test_pair_search() -> Result<()> {
let config = Arc::new(AgentConfig::default());
let mut a = Agent::new(Instant::now(), config, test_crypto_provider())?;
assert!(
a.candidate_pairs.is_empty(),
"TestPairSearch is only a valid test if a.validPairs is empty on construction"
);
let cp = a.get_best_available_pair();
assert!(cp.is_none(), "No Candidate pairs should exist");
a.close()?;
Ok(())
}
#[test]
fn test_pair_priority() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.1".to_owned(),
port: 19216,
component: 1,
..Default::default()
},
..Default::default()
};
let host_local = host_config.new_candidate_host()?;
a.local_candidates.push(host_local);
let relay_config = CandidateRelayConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "1.2.3.4".to_owned(),
port: 12340,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 43210,
..Default::default()
};
let relay_remote = relay_config.new_candidate_relay()?;
let srflx_config = CandidateServerReflexiveConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "10.10.10.2".to_owned(),
port: 19218,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 43212,
..Default::default()
};
let srflx_remote = srflx_config.new_candidate_server_reflexive()?;
let prflx_config = CandidatePeerReflexiveConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "10.10.10.2".to_owned(),
port: 19217,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 43211,
..Default::default()
};
let prflx_remote = prflx_config.new_candidate_peer_reflexive()?;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "1.2.3.5".to_owned(),
port: 12350,
component: 1,
..Default::default()
},
..Default::default()
};
let host_remote = host_config.new_candidate_host()?;
let remotes: Vec<Candidate> = vec![relay_remote, srflx_remote, prflx_remote, host_remote];
for remote in remotes {
a.remote_candidates.push(remote);
}
{
let local = 0;
for remote in 0..a.remote_candidates.len() {
if a.find_pair(local, remote).is_none() {
a.add_pair(local, remote);
}
if let Some(p) = a.find_pair(local, remote) {
a.candidate_pairs[p].state = CandidatePairState::Succeeded;
}
if let Some(best_pair) = a.get_best_available_pair() {
assert_eq!(
a.candidate_pairs[best_pair].to_string(),
CandidatePair::new(
local,
remote,
a.local_candidates[local].priority(),
a.remote_candidates[remote].priority(),
a.is_controlling,
)
.to_string(),
"Unexpected bestPair {best_pair} (expected remote: {remote})",
);
} else {
panic!("expected Some, but got None");
}
}
}
a.close()?;
Ok(())
}
fn pipe(
default_config0: Option<AgentConfig>,
default_config1: Option<AgentConfig>,
) -> Result<(Agent, Agent)> {
let mut cfg0 = if let Some(cfg) = default_config0 {
cfg
} else {
AgentConfig::default()
};
cfg0.urls = vec![];
let a_agent = Agent::new(Instant::now(), Arc::new(cfg0), test_crypto_provider())?;
let mut cfg1 = if let Some(cfg) = default_config1 {
cfg
} else {
AgentConfig::default()
};
cfg1.urls = vec![];
let b_agent = Agent::new(Instant::now(), Arc::new(cfg1), test_crypto_provider())?;
Ok((a_agent, b_agent))
}
#[test]
fn test_on_selected_candidate_pair_change() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.1".to_owned(),
port: 19216,
component: 1,
..Default::default()
},
..Default::default()
};
let host_local = host_config.new_candidate_host()?;
a.add_local_candidate(host_local)?;
let relay_config = CandidateRelayConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "1.2.3.4".to_owned(),
port: 12340,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 43210,
..Default::default()
};
let relay_remote = relay_config.new_candidate_relay()?;
a.add_remote_candidate(relay_remote)?;
let (local, remote) = (0, 0);
a.add_pair(local, remote);
a.set_selected_pair(Some(Instant::now()), Some(0));
let mut is_selected_candidate_pair_change_event_fired = false;
while let Some(event) = a.poll_event() {
if let Event::SelectedCandidatePairChange(_, _) = event.event {
is_selected_candidate_pair_change_event_fired = true;
}
}
assert!(is_selected_candidate_pair_change_event_fired);
a.close()?;
Ok(())
}
#[test]
fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 777,
component: 1,
..Default::default()
},
..Default::default()
};
let local_candidate = host_config.new_candidate_host()?;
let local = 0;
let local_priority = local_candidate.priority();
let local_network_type = local_candidate.network_type();
a.add_local_candidate(local_candidate)?;
let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
let (username, local_pwd, tie_breaker) = {
a.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "".to_string(),
pwd: "".to_string(),
});
(
a.ufrag_pwd.local_credentials.ufrag.to_owned() + ":",
a.ufrag_pwd.local_credentials.pwd.clone(),
a.tie_breaker,
)
};
let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, username)),
Box::new(UseCandidateAttr::new()),
Box::new(AttrControlling(tie_breaker)),
Box::new(PriorityAttr(local_priority)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
local_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
{
a.handle_inbound(Instant::now(), &mut msg, local, remote_addr)?;
assert_eq!(
a.remote_candidates.len(),
1,
"failed to add a network type to the remote candidate list"
);
if let Some(remote_index) = a.find_remote_candidate(remote_addr) {
let c = &a.remote_candidates[remote_index];
assert_eq!(
c.candidate_type(),
CandidateType::PeerReflexive,
"candidate type must be prflx"
);
assert_eq!(c.address(), "172.17.0.3", "IP address mismatch");
assert_eq!(c.port(), 999, "Port number mismatch");
} else {
assert!(
false,
"expected non-empty remote candidate for network type {}",
local_network_type,
);
}
}
a.close()?;
Ok(())
}
#[test]
fn test_handle_peer_reflexive_unknown_remote() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let mut tid = TransactionId::default();
tid.0[..3].copy_from_slice("ABC".as_bytes());
let remote_pwd = {
a.pending_binding_requests = vec![BindingRequest {
timestamp: Instant::now(),
transaction_id: tid,
destination: SocketAddr::from_str("0.0.0.0:0")?,
is_use_candidate: false,
}];
a.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "".to_string(),
pwd: "".to_string(),
});
"".to_string()
};
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 777,
component: 1,
..Default::default()
},
..Default::default()
};
let local = host_config.new_candidate_host()?;
let local_index = 0;
a.add_local_candidate(local)?;
let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_SUCCESS),
Box::new(tid),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
remote_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
let result = a.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
assert!(result.is_err());
assert_eq!(
a.remote_candidates.len(),
0,
"unknown remote was able to create a candidate"
);
a.close()?;
Ok(())
}
#[test]
fn test_zero_failed_timeout_keeps_checking_agent_recoverable() -> Result<()> {
let base = Instant::now();
let mut agent = Agent::new(
base,
Arc::new(AgentConfig {
disconnected_timeout: Some(Duration::from_secs(5)),
failed_timeout: Some(Duration::ZERO),
..Default::default()
}),
test_crypto_provider(),
)?;
agent.connection_state = ConnectionState::Checking;
agent.last_connection_state = ConnectionState::Checking;
agent.checking_duration = base;
agent.contact(base + Duration::from_secs(3600));
assert_eq!(
agent.connection_state,
ConnectionState::Checking,
"a zero failed timeout is documented to disable the terminal transition"
);
Ok(())
}
#[test]
fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<()> {
let mut config = AgentConfig::default();
config.is_controlling = true;
let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
agent.tie_breaker = 100;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 7777,
component: 1,
..Default::default()
},
..Default::default()
};
let local_candidate = host_config.new_candidate_host()?;
let local_index = 0;
agent.add_local_candidate(local_candidate)?;
let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
let remote_host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "172.17.0.3".to_owned(),
port: 9999,
component: 1,
..Default::default()
},
..Default::default()
};
let remote_candidate = remote_host_config.new_candidate_host()?;
agent.add_remote_candidate(remote_candidate)?;
agent.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "remote_ufrag".to_string(),
pwd: "remote_pwd".to_string(),
});
let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
let remote_tiebreaker = 200; let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, username)),
Box::new(AttrControlling(remote_tiebreaker)), Box::new(PriorityAttr(1000)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
local_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
assert!(
agent.is_controlling,
"Agent should be controlling before role conflict"
);
let initial_pair_count = agent.candidate_pairs.len();
let initial_priorities: Vec<u64> = agent.candidate_pairs.iter().map(|p| p.priority()).collect();
let result = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
let _ = result;
assert!(
!agent.is_controlling,
"Agent should have switched to controlled role (smaller tiebreaker)"
);
if !agent.candidate_pairs.is_empty() {
for pair in &agent.candidate_pairs {
assert!(
!pair.ice_role_controlling,
"Candidate pair should have controlling role set to false"
);
}
if initial_pair_count > 0 && initial_priorities.len() > 0 {
assert_eq!(
agent.candidate_pairs.len(),
initial_pair_count,
"Number of pairs should remain the same"
);
}
}
assert!(
agent.nominated_pair.is_none(),
"Nominated pair should be cleared after role switch"
);
agent.close()?;
Ok(())
}
#[test]
fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> {
let mut config = AgentConfig::default();
config.is_controlling = true;
let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
agent.tie_breaker = 500;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 7777,
component: 1,
..Default::default()
},
..Default::default()
};
let local_candidate = host_config.new_candidate_host()?;
let local_index = 0;
agent.add_local_candidate(local_candidate)?;
let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
let remote_host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "172.17.0.3".to_owned(),
port: 9999,
component: 1,
..Default::default()
},
..Default::default()
};
let remote_candidate = remote_host_config.new_candidate_host()?;
agent.add_remote_candidate(remote_candidate)?;
agent.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "remote_ufrag".to_string(),
pwd: "remote_pwd".to_string(),
});
let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
let remote_tiebreaker = 100; let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, username)),
Box::new(AttrControlling(remote_tiebreaker)), Box::new(PriorityAttr(1000)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
local_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
assert!(
agent.is_controlling,
"Agent should be controlling before role conflict"
);
let _ = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
assert!(
agent.is_controlling,
"Agent should remain controlling (larger tiebreaker)"
);
for pair in &agent.candidate_pairs {
assert!(
pair.ice_role_controlling,
"Candidate pair should still have controlling role set to true"
);
}
agent.close()?;
Ok(())
}
#[test]
fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> {
use crate::attributes::control::AttrControlled;
let mut config = AgentConfig::default();
config.is_controlling = false; let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
agent.tie_breaker = 500;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 7777,
component: 1,
..Default::default()
},
..Default::default()
};
let local_candidate = host_config.new_candidate_host()?;
let local_index = 0;
agent.add_local_candidate(local_candidate)?;
let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
let remote_host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "172.17.0.3".to_owned(),
port: 9999,
component: 1,
..Default::default()
},
..Default::default()
};
let remote_candidate = remote_host_config.new_candidate_host()?;
agent.add_remote_candidate(remote_candidate)?;
agent.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "remote_ufrag".to_string(),
pwd: "remote_pwd".to_string(),
});
let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
let remote_tiebreaker = 100; let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, username)),
Box::new(AttrControlled(remote_tiebreaker)), Box::new(PriorityAttr(1000)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
local_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
assert!(
!agent.is_controlling,
"Agent should be controlled before role conflict"
);
let _ = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
assert!(
agent.is_controlling,
"Agent should have switched to controlling role (larger tiebreaker)"
);
for pair in &agent.candidate_pairs {
assert!(
pair.ice_role_controlling,
"Candidate pair should have controlling role set to true"
);
}
assert!(
agent.nominated_pair.is_none(),
"Nominated pair should be cleared after role switch"
);
agent.close()?;
Ok(())
}
#[test]
fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> {
use crate::attributes::control::AttrControlled;
let mut config = AgentConfig::default();
config.is_controlling = false; let mut agent = Agent::new(Instant::now(), Arc::new(config), test_crypto_provider())?;
agent.tie_breaker = 100;
let host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 7777,
component: 1,
..Default::default()
},
..Default::default()
};
let local_candidate = host_config.new_candidate_host()?;
let local_index = 0;
agent.add_local_candidate(local_candidate)?;
let remote_addr = SocketAddr::from_str("172.17.0.3:9999")?;
let remote_host_config = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "172.17.0.3".to_owned(),
port: 9999,
component: 1,
..Default::default()
},
..Default::default()
};
let remote_candidate = remote_host_config.new_candidate_host()?;
agent.add_remote_candidate(remote_candidate)?;
agent.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "remote_ufrag".to_string(),
pwd: "remote_pwd".to_string(),
});
let username = agent.ufrag_pwd.local_credentials.ufrag.clone() + ":remote_ufrag";
let local_pwd = agent.ufrag_pwd.local_credentials.pwd.clone();
let remote_tiebreaker = 500; let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, username)),
Box::new(AttrControlled(remote_tiebreaker)), Box::new(PriorityAttr(1000)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
local_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
assert!(
!agent.is_controlling,
"Agent should be controlled before role conflict"
);
let _ = agent.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr);
assert!(
!agent.is_controlling,
"Agent should remain controlled (smaller tiebreaker)"
);
for pair in &agent.candidate_pairs {
assert!(
!pair.ice_role_controlling,
"Candidate pair should still have controlling role set to false"
);
}
agent.close()?;
Ok(())
}
#[test]
fn test_candidate_type_filtering() -> Result<()> {
let config = Arc::new(AgentConfig {
candidate_types: vec![CandidateType::Relay],
..Default::default()
});
let mut agent = Agent::new(Instant::now(), config, test_crypto_provider())?;
let host_local = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.1".to_owned(),
port: 5000,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
assert!(
!agent.add_local_candidate(host_local)?,
"Host local candidate should be filtered out"
);
let host_remote = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.3".to_owned(),
port: 5003,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
assert!(
agent.add_remote_candidate(host_remote)?,
"Remote candidates should be accepted regardless of candidate_types"
);
let relay_local = CandidateRelayConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "1.2.3.4".to_owned(),
port: 5004,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 5005,
..Default::default()
}
.new_candidate_relay()?;
assert!(
agent.add_local_candidate(relay_local)?,
"Relay local candidate should be accepted"
);
assert_eq!(
agent.local_candidates.len(),
1,
"Only the relay candidate should be stored"
);
assert_eq!(
agent.remote_candidates.len(),
1,
"Remote host candidate should be stored"
);
agent.close()?;
Ok(())
}
#[test]
fn test_keepalive_sent_during_media_flow() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let host_local = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.1".to_owned(),
port: 19216,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_local_candidate(host_local)?;
let relay_remote = CandidateRelayConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "1.2.3.4".to_owned(),
port: 12340,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 43210,
..Default::default()
}
.new_candidate_relay()?;
a.add_remote_candidate(relay_remote)?;
a.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "remoteufrag".to_string(),
pwd: "remotepwd".to_string(),
});
a.is_controlling = true;
a.add_pair(0, 0);
a.set_selected_pair(Some(Instant::now()), Some(0));
a.local_candidates[0].seen(Instant::now(), true);
a.remote_candidates[0].seen(Instant::now(), false);
let base = Instant::now();
let keepalive_interval = a.keepalive_interval;
a.last_consent_sent = base - Duration::from_secs(10);
a.write_outs.clear();
a.check_keepalive(base);
assert!(
!a.write_outs.is_empty(),
"check_keepalive must send a STUN ping even when media timestamps are fresh"
);
a.write_outs.clear();
a.check_keepalive(base);
assert!(
a.write_outs.is_empty(),
"check_keepalive must not send again before keepalive_interval elapses"
);
a.check_keepalive(base + keepalive_interval + Duration::from_millis(1));
assert!(
!a.write_outs.is_empty(),
"check_keepalive must send again once keepalive_interval has elapsed"
);
a.close()?;
Ok(())
}
#[test]
fn test_pair_network_type_mismatch() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let local_v4 = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.1".to_owned(),
port: 7777,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_local_candidate(local_v4)?;
let remote_v6 = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "2001:db8::1".to_owned(),
port: 8888,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_remote_candidate(remote_v6)?;
assert!(
a.candidate_pairs.is_empty(),
"IPv4 local and IPv6 remote candidates should not be paired"
);
let remote_v4 = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.1.2".to_owned(),
port: 8888,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_remote_candidate(remote_v4)?;
assert_eq!(
a.candidate_pairs.len(),
1,
"IPv4 local and IPv4 remote candidates should form exactly one pair"
);
let local_tcp4_active = CandidateHostConfig {
base_config: CandidateConfig {
network: "tcp".to_owned(),
address: "192.168.1.3".to_owned(),
port: 7778,
component: 1,
..Default::default()
},
tcp_type: TcpType::Active,
}
.new_candidate_host()?;
a.add_local_candidate(local_tcp4_active)?;
let remote_tcp6_passive = CandidateHostConfig {
base_config: CandidateConfig {
network: "tcp".to_owned(),
address: "2001:db8::2".to_owned(),
port: 8889,
component: 1,
..Default::default()
},
tcp_type: TcpType::Passive,
}
.new_candidate_host()?;
a.add_remote_candidate(remote_tcp6_passive)?;
assert_eq!(
a.candidate_pairs.len(),
1,
"TCP4 active local and TCP6 passive remote should not create an additional pair"
);
let remote_tcp4_passive = CandidateHostConfig {
base_config: CandidateConfig {
network: "tcp".to_owned(),
address: "192.168.1.4".to_owned(),
port: 8890,
component: 1,
..Default::default()
},
tcp_type: TcpType::Passive,
}
.new_candidate_host()?;
a.add_remote_candidate(remote_tcp4_passive)?;
assert_eq!(
a.candidate_pairs.len(),
2,
"TCP4 active local and TCP4 passive remote should form exactly one pair"
);
a.close()?;
Ok(())
}
#[test]
fn test_transition_to_failed_clears_stale_candidate_pairs() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let local = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 777,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_local_candidate(local)?;
let remote = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "172.17.0.3".to_owned(),
port: 999,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_remote_candidate(remote)?;
assert_eq!(a.candidate_pairs.len(), 1);
a.set_selected_pair(Some(Instant::now()), Some(0));
a.nominated_pair = Some(0);
a.update_connection_state(Some(Instant::now()), ConnectionState::Failed);
assert!(
a.candidate_pairs.is_empty(),
"candidate pairs must be cleared when candidates are deleted"
);
assert!(a.selected_pair.is_none(), "selected_pair must be reset");
assert!(a.nominated_pair.is_none(), "nominated_pair must be reset");
assert!(a.local_candidates.is_empty());
assert!(a.remote_candidates.is_empty());
assert!(a.get_selected_candidate_pair().is_none());
assert!(a.get_best_available_candidate_pair().is_none());
a.close()?;
Ok(())
}
#[test]
fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> {
use sansio::Protocol as _;
let cfg = AgentConfig {
disconnected_timeout: Some(Duration::from_secs(1)),
failed_timeout: Some(Duration::from_secs(1)),
..Default::default()
};
let mut a = Agent::new(Instant::now(), Arc::new(cfg), test_crypto_provider())?;
let local_candidate = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 777,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
let local_index = 0;
let local_priority = local_candidate.priority();
a.add_local_candidate(local_candidate)?;
a.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: "".to_string(),
pwd: "".to_string(),
});
let username = a.ufrag_pwd.local_credentials.ufrag.clone() + ":";
let local_pwd = a.ufrag_pwd.local_credentials.pwd.clone();
let tie_breaker = a.tie_breaker;
a.connection_state = ConnectionState::Checking;
a.last_connection_state = ConnectionState::Checking;
a.checking_duration = Instant::now()
.checked_sub(Duration::from_secs(3600))
.unwrap_or(a.start_time);
let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, username)),
Box::new(UseCandidateAttr::new()),
Box::new(AttrControlling(tie_breaker)),
Box::new(PriorityAttr(local_priority)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
local_pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
a.handle_inbound(Instant::now(), &mut msg, local_index, remote_addr)?;
assert_eq!(
a.remote_candidates.len(),
1,
"peer-reflexive remote candidate should have been added"
);
assert_eq!(
a.connection_state,
ConnectionState::Checking,
"connection state must not change while handling the inbound request"
);
assert!(
a.force_candidate_contact,
"a connectivity check should have been deferred to the timeout handler"
);
a.handle_timeout(Instant::now())?;
assert_eq!(a.connection_state, ConnectionState::Failed);
assert!(a.candidate_pairs.is_empty());
assert!(a.local_candidates.is_empty());
assert!(a.remote_candidates.is_empty());
assert!(a.selected_pair.is_none());
assert!(a.nominated_pair.is_none());
a.close()?;
Ok(())
}
#[test]
fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> {
let cand_line =
"1114572465 1 udp 2113939711 61b445d2-6503-41ac-96ce-ee3edac00e9f.local 61163 typ host";
let base = Instant::now();
let mut agent = Agent::new(
base,
Arc::new(AgentConfig {
multicast_dns_mode: crate::mdns::MulticastDnsMode::QueryOnly,
..Default::default()
}),
test_crypto_provider(),
)?;
let added = agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?;
assert!(
!added,
"an mDNS candidate is not immediately usable; resolution is async"
);
let deadline = agent
.poll_timeout()
.expect("a scheduled mDNS query must ask the driver for a timeout");
assert!(
deadline <= base,
"the scheduled query is already due, so the driver runs it immediately"
);
agent.handle_timeout(deadline)?;
let pkt = agent
.poll_write()
.expect("QueryOnly must emit an mDNS query for a .local remote candidate");
assert_eq!(
pkt.transport.peer_addr.port(),
mdns::MDNS_PORT,
"the emitted packet must be an mDNS query"
);
assert_eq!(
pkt.now, deadline,
"the query is stamped with the instant the driver supplied, not an ambient one"
);
let mut agent = Agent::new(
base,
Arc::new(AgentConfig {
multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled,
..Default::default()
}),
test_crypto_provider(),
)?;
agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?;
assert!(
agent.poll_write().is_none(),
"Disabled must not emit an mDNS query for a .local remote candidate"
);
Ok(())
}
#[test]
fn test_failed_agent_stops_and_restart_resumes_connectivity_check_timer() -> Result<()> {
let base = Instant::now();
let mut agent = Agent::new(
base,
Arc::new(AgentConfig {
multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled,
..Default::default()
}),
test_crypto_provider(),
)?;
agent.start_connectivity_checks(
base,
true,
"remote-ufrag".to_owned(),
"remote-password".to_owned(),
)?;
agent.update_connection_state(
Some(base + Duration::from_secs(190)),
ConnectionState::Failed,
);
assert_eq!(
agent.poll_timeout(),
None,
"a failed agent has no more connectivity checks to schedule"
);
agent.handle_timeout(base + Duration::from_secs(191))?;
assert_eq!(
agent.poll_timeout(),
None,
"handling time after failure must not recreate the stale deadline"
);
let restart_time = base + Duration::from_secs(200);
agent.apply_restart(restart_time, true)?;
agent.start_connectivity_checks(
restart_time,
true,
"new-remote-ufrag".to_owned(),
"new-remote-password".to_owned(),
)?;
assert!(
agent.poll_timeout().is_some(),
"an ICE restart must resume connectivity-check scheduling"
);
agent.close()?;
assert_eq!(
agent.poll_timeout(),
None,
"a closed agent has no more connectivity checks to schedule"
);
Ok(())
}
#[test]
fn test_send_stun_from_srflx_uses_base_addr() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let srflx_local = CandidateServerReflexiveConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "10.79.12.1".to_owned(), port: 60823,
component: 1,
..Default::default()
},
rel_addr: "192.168.0.2".to_owned(), rel_port: 5000,
..Default::default()
}
.new_candidate_server_reflexive()?;
a.add_local_candidate(srflx_local)?;
let host_remote = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "10.79.11.1".to_owned(),
port: 37983,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_remote_candidate(host_remote)?;
a.write_outs.clear();
let msg = Message::new();
a.send_stun(Instant::now(), &msg, 0, 0);
let transmit = a.write_outs.pop_front().expect("send_stun must emit");
assert_eq!(
transmit.transport.local_addr,
"192.168.0.2:5000".parse().unwrap(),
"srflx checks must be sent from the candidate's base, not the mapped address"
);
assert_eq!(
transmit.transport.peer_addr,
"10.79.11.1:37983".parse().unwrap()
);
a.close()?;
Ok(())
}
#[test]
fn test_staged_ice_restart_keeps_live_session_authenticating() -> Result<()> {
let mut a = Agent::new(
Instant::now(),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let local_candidate = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 777,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
let local_priority = local_candidate.priority();
a.add_local_candidate(local_candidate)?;
a.ufrag_pwd.remote_credentials = Some(Credentials {
ufrag: String::new(),
pwd: String::new(),
});
let original = a.ufrag_pwd.local_credentials.clone();
let remote_addr = SocketAddr::from_str("172.17.0.3:999")?;
let inbound = |ufrag: &str, pwd: String, tie_breaker: u64| -> Result<Message> {
let mut msg = Message::new();
msg.build(&[
Box::new(BINDING_REQUEST),
Box::new(TransactionId::new()),
Box::new(Username::new(ATTR_USERNAME, format!("{ufrag}:"))),
Box::new(AttrControlling(tie_breaker)),
Box::new(PriorityAttr(local_priority)),
Box::new(MessageIntegrity::new_short_term_integrity_with_provider(
pwd,
test_crypto_provider().crypto(),
)),
Box::new(FINGERPRINT),
])?;
Ok(msg)
};
let mut msg = inbound(&original.ufrag, original.pwd.clone(), a.tie_breaker)?;
a.handle_inbound(Instant::now(), &mut msg, 0, remote_addr)?;
a.generate_restart_credentials(String::new(), String::new())?;
let advertised = a.get_local_credentials().clone();
assert_ne!(
advertised.ufrag, original.ufrag,
"staging a restart must generate a fresh ufrag for the offer"
);
assert_ne!(advertised.pwd, original.pwd);
assert_eq!(
a.ufrag_pwd.local_credentials.ufrag, original.ufrag,
"staging must not install the new ufrag into the live session"
);
assert_eq!(
a.ufrag_pwd.local_credentials.pwd, original.pwd,
"staging must not install the new pwd into the live session"
);
let mut msg = inbound(&original.ufrag, original.pwd.clone(), a.tie_breaker)?;
a.handle_inbound(Instant::now(), &mut msg, 0, remote_addr)
.expect("inbound STUN keyed on the pre-restart credentials must still validate");
assert!(a.has_pending_restart());
let mut msg = inbound(&original.ufrag, original.pwd.clone(), a.tie_breaker)?;
a.handle_inbound(Instant::now(), &mut msg, 0, remote_addr)
.expect("a discarded ICE-restart offer must leave the session working");
a.apply_restart(Instant::now(), true)?;
assert!(!a.has_pending_restart());
assert_eq!(
a.ufrag_pwd.local_credentials.ufrag, advertised.ufrag,
"apply_restart must install exactly the ufrag the offer advertised"
);
assert_eq!(
a.ufrag_pwd.local_credentials.pwd, advertised.pwd,
"apply_restart must install exactly the pwd the offer advertised"
);
a.close()?;
Ok(())
}
#[test]
fn test_agent_events_carry_the_instant_they_were_observed_at() -> Result<()> {
let base = Instant::now();
let t = |secs| base + Duration::from_secs(secs);
let mut a = Agent::new(
t(0),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let host_local = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 1000,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_local_candidate(host_local)?;
let relay_remote = CandidateRelayConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "1.2.3.4".to_owned(),
port: 12340,
component: 1,
..Default::default()
},
rel_addr: "4.3.2.1".to_owned(),
rel_port: 43210,
..Default::default()
}
.new_candidate_relay()?;
a.add_remote_candidate(relay_remote)?;
a.add_pair(0, 0);
a.set_selected_pair(Some(t(5)), Some(0));
let stamped: Vec<_> = std::iter::from_fn(|| a.poll_event()).collect();
assert!(!stamped.is_empty(), "selecting a pair emits events");
for e in &stamped {
assert_eq!(
e.now,
t(5),
"event {:?} must carry the instant its condition was observed at",
std::mem::discriminant(&e.event)
);
}
a.handle_timeout(t(30))?;
while a.poll_event().is_some() {}
a.close()?;
assert_eq!(a.connection_state, ConnectionState::Closed);
assert!(
a.poll_event().is_none(),
"close emits no event, having no instant to stamp one with"
);
Ok(())
}
#[test]
fn test_stats_snapshot_reports_the_callers_instant_throughout() -> Result<()> {
let base = Instant::now();
let t = |secs| base + Duration::from_secs(secs);
let mut a = Agent::new(
t(0),
Arc::new(AgentConfig::default()),
test_crypto_provider(),
)?;
let host_local = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.2".to_owned(),
port: 1000,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_local_candidate(host_local)?;
let host_remote = CandidateHostConfig {
base_config: CandidateConfig {
network: "udp".to_owned(),
address: "192.168.0.3".to_owned(),
port: 1001,
component: 1,
..Default::default()
},
..Default::default()
}
.new_candidate_host()?;
a.add_remote_candidate(host_remote)?;
let pairs = a.get_candidate_pairs_stats(t(600));
assert!(!pairs.is_empty(), "adding a remote candidate forms a pair");
for p in &pairs {
for (field, stamp) in [
("timestamp", p.timestamp),
("last_packet_sent_timestamp", p.last_packet_sent_timestamp),
(
"last_packet_received_timestamp",
p.last_packet_received_timestamp,
),
("first_request_timestamp", p.first_request_timestamp),
("last_request_timestamp", p.last_request_timestamp),
("last_response_timestamp", p.last_response_timestamp),
("consent_expired_timestamp", p.consent_expired_timestamp),
] {
assert_eq!(stamp, t(600), "{field} must report the caller's instant");
}
}
let locals = a.get_local_candidates_stats(t(600));
assert_eq!(locals.len(), 1);
assert_eq!(locals[0].timestamp, t(600));
let remotes = a.get_remote_candidates_stats(t(600));
assert_eq!(remotes.len(), 1);
assert_eq!(remotes[0].timestamp, t(600));
assert_eq!(a.get_candidate_pairs_stats(t(900))[0].timestamp, t(900));
Ok(())
}