use crate::IceError;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::net::{IpAddr, SocketAddr};
use str0m_proto::{Protocol, TcpType};
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Candidate {
foundation: Option<String>,
component_id: u16,
proto: Protocol,
prio: Option<u32>,
addr: SocketAddr,
base: Option<SocketAddr>,
kind: CandidateKind,
tcptype: Option<TcpType>,
raddr: Option<SocketAddr>,
ufrag: Option<String>,
local: SocketAddr,
local_preference: Option<u32>,
discarded: bool,
}
impl fmt::Debug for Candidate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Candidate({}={}/{}", self.kind, self.addr, self.proto)?;
if let Some(base) = self.base {
if base != self.addr {
write!(f, " base={base}")?;
}
}
if let Some(raddr) = self.raddr {
write!(f, " raddr={raddr}")?;
}
write!(f, " prio={}", self.prio())?;
if self.discarded {
write!(f, " discarded")?;
}
write!(f, ")")
}
}
impl Candidate {
pub fn builder() -> CandidateBuilder<NoProtocol, NoRoute> {
CandidateBuilder {
protocol_state: NoProtocol(()),
route_state: NoRoute(()),
foundation: None,
component_id: 1, prio: None,
tcptype: None,
raddr: None,
ufrag: None,
local_preference: None,
}
}
#[allow(clippy::too_many_arguments)]
fn new(
foundation: Option<String>,
component_id: u16,
proto: Protocol,
prio: Option<u32>,
addr: SocketAddr,
base: Option<SocketAddr>,
kind: CandidateKind,
raddr: Option<SocketAddr>,
tcptype: Option<TcpType>,
ufrag: Option<String>,
local: SocketAddr,
) -> Self {
Candidate {
foundation,
component_id,
proto,
prio,
addr,
base,
kind,
raddr,
tcptype,
ufrag,
local,
local_preference: None,
discarded: false,
}
}
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
foundation: String,
component_id: u16,
proto: Protocol,
prio: u32,
addr: SocketAddr,
kind: CandidateKind,
raddr: Option<SocketAddr>,
tcptype: Option<TcpType>,
ufrag: Option<String>,
) -> Self {
Candidate::new(
Some(foundation),
component_id,
proto,
Some(prio),
addr,
None,
kind,
raddr,
tcptype,
ufrag,
match kind {
CandidateKind::Host => addr,
CandidateKind::PeerReflexive
| CandidateKind::ServerReflexive
| CandidateKind::Relayed => Self::arbitrary_raddr(addr),
},
)
}
pub fn host(addr: SocketAddr, proto: impl TryInto<Protocol>) -> Result<Self, IceError> {
if !is_valid_ip(addr.ip()) {
return Err(IceError::BadCandidate(format!("invalid ip {}", addr.ip())));
}
Ok(Candidate::new(
None,
1, parse_proto(proto)?,
None,
addr,
Some(addr),
CandidateKind::Host,
None,
None,
None,
addr,
))
}
pub fn server_reflexive(
addr: SocketAddr,
base: SocketAddr,
proto: impl TryInto<Protocol>,
) -> Result<Self, IceError> {
if !is_valid_ip(addr.ip()) {
return Err(IceError::BadCandidate(format!("invalid ip {}", addr.ip())));
}
if addr.is_ipv4() != base.is_ipv4() {
return Err(IceError::BadCandidate(
"addr and base are different IP versions".to_owned(),
));
}
Ok(Candidate::new(
None,
1, parse_proto(proto)?,
None,
addr,
Some(base),
CandidateKind::ServerReflexive,
Some(Self::arbitrary_raddr(addr)),
None,
None,
base,
))
}
pub fn relayed(
addr: SocketAddr,
local: SocketAddr,
proto: impl TryInto<Protocol>,
) -> Result<Self, IceError> {
if !is_valid_ip(addr.ip()) {
return Err(IceError::BadCandidate(format!("invalid ip {}", addr.ip())));
}
Ok(Candidate::new(
None,
1, parse_proto(proto)?,
None,
addr,
Some(addr),
CandidateKind::Relayed,
Some(Self::arbitrary_raddr(addr)),
None,
None,
local,
))
}
pub fn from_sdp_string(s: &str) -> Result<Self, IceError> {
crate::parse::parse_candidate(s)
}
pub(crate) fn peer_reflexive(
proto: impl TryInto<Protocol>,
addr: SocketAddr,
base: SocketAddr,
prio: u32,
found: Option<String>,
ufrag: String,
) -> Self {
Candidate::new(
found,
1, parse_proto(proto).expect("internal call to have correct protocol"),
Some(prio),
addr,
Some(base),
CandidateKind::PeerReflexive,
None,
None,
Some(ufrag),
base,
)
}
fn arbitrary_raddr(s: SocketAddr) -> SocketAddr {
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
match s {
SocketAddr::V4(_) => SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
SocketAddr::V6(_) => SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)),
}
}
#[cfg(test)]
pub(crate) fn test_peer_rflx(
addr: SocketAddr,
base: SocketAddr,
proto: impl TryInto<Protocol>,
) -> Self {
Candidate::new(
None,
1, parse_proto(proto).expect("internal test to have correct protocol"),
None,
addr,
Some(base),
CandidateKind::PeerReflexive,
None,
None,
None,
base,
)
}
pub(crate) fn foundation(&self) -> String {
if let Some(v) = &self.foundation {
return v.clone();
}
let mut hasher = DefaultHasher::new();
self.kind.hash(&mut hasher);
self.base().ip().hash(&mut hasher);
if let Some(raddr) = self.raddr {
raddr.ip().hash(&mut hasher);
}
self.proto.hash(&mut hasher);
let hash = hasher.finish();
format!("{:08x}{hash:x}", self.prio().to_be())
}
pub fn prio(&self) -> u32 {
self.do_prio(false)
}
pub(crate) fn prio_prflx(&self) -> u32 {
self.do_prio(true)
}
fn do_prio(&self, as_prflx: bool) -> u32 {
if let Some(prio) = &self.prio {
return *prio;
}
let kind = if as_prflx {
CandidateKind::PeerReflexive
} else {
self.kind
};
let type_preference = match (kind, self.proto) {
(CandidateKind::Host, Protocol::Udp) => 126,
(CandidateKind::PeerReflexive, Protocol::Udp) => 110,
(CandidateKind::ServerReflexive, _) => 100,
(CandidateKind::Host, _) => 90,
(CandidateKind::PeerReflexive, _) => 80,
(CandidateKind::Relayed, Protocol::Udp) => 2,
(CandidateKind::Relayed, Protocol::Tcp) => 1,
(CandidateKind::Relayed, _) => 0,
};
let prio =
type_preference << 24 | self.local_preference() << 8 | (256 - self.component_id as u32);
assert!(prio >= 1 && prio < 2_u32.pow(31));
prio
}
pub(crate) fn local_preference(&self) -> u32 {
self.local_preference
.unwrap_or_else(|| if self.addr.is_ipv6() { 65_535 } else { 65_534 })
}
pub(crate) fn component_id(&self) -> u16 {
self.component_id
}
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn local(&self) -> SocketAddr {
self.local
}
pub fn proto(&self) -> Protocol {
self.proto
}
pub(crate) fn base(&self) -> SocketAddr {
self.base.unwrap_or(self.addr)
}
pub(crate) fn raddr(&self) -> Option<SocketAddr> {
self.raddr
}
pub fn tcptype(&self) -> Option<TcpType> {
self.tcptype
}
pub fn kind(&self) -> CandidateKind {
self.kind
}
pub(crate) fn set_local_preference(&mut self, v: u32) {
self.local_preference = Some(v);
}
pub(crate) fn set_discarded(&mut self, discarded: bool) {
self.discarded = discarded;
}
pub(crate) fn discarded(&self) -> bool {
self.discarded
}
pub fn set_ufrag(&mut self, ufrag: &str) {
self.ufrag = Some(ufrag.into());
}
pub fn ufrag(&self) -> Option<&str> {
self.ufrag.as_deref()
}
pub(crate) fn clear_ufrag(&mut self) {
self.ufrag = None;
}
pub fn to_sdp_string(&self) -> String {
let mut s = format!(
"candidate:{} {} {} {} {} {} typ {}",
self.foundation(),
self.component_id,
self.proto,
self.prio(),
self.addr.ip(),
self.addr.port(),
self.kind
);
if let Some(raddr) = &self.raddr {
s.push_str(&format!(" raddr {} rport {}", raddr.ip(), raddr.port()))
}
if let Some(tcptype) = &self.tcptype {
s.push_str(&format!(" tcptype {}", tcptype));
}
if let Some(ufrag) = &self.ufrag {
s.push_str(&format!(" ufrag {}", ufrag));
}
s
}
}
impl fmt::Display for Candidate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_sdp_string())
}
}
fn parse_proto(proto: impl TryInto<Protocol>) -> Result<Protocol, IceError> {
proto
.try_into()
.map_err(|_| IceError::BadCandidate("invalid protocol".into()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CandidateKind {
Host,
PeerReflexive,
ServerReflexive,
Relayed,
}
impl fmt::Display for CandidateKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let x = match self {
CandidateKind::Host => "host",
CandidateKind::PeerReflexive => "prflx",
CandidateKind::ServerReflexive => "srflx",
CandidateKind::Relayed => "relay",
};
write!(f, "{x}")
}
}
fn is_valid_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v) => {
!v.is_link_local() && !v.is_broadcast() && !v.is_multicast() && !v.is_unspecified()
}
IpAddr::V6(v) => !v.is_multicast() && !v.is_unspecified(),
}
}
impl Serialize for Candidate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut o = serializer.serialize_struct("CandidateInfo", 4)?;
o.serialize_field("candidate", &self.to_sdp_string())?;
o.serialize_field("sdpMid", &None::<()>)?;
o.serialize_field("sdpMLineIndex", &0)?;
o.serialize_field("usernameFragment", &self.ufrag())?;
o.end()
}
}
impl<'de> Deserialize<'de> for Candidate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CandidateInfo {
candidate: String,
username_fragment: Option<String>,
}
let CandidateInfo {
candidate,
username_fragment,
} = CandidateInfo::deserialize(deserializer)?;
let mut candidate =
Candidate::from_sdp_string(&candidate).map_err(serde::de::Error::custom)?;
if let Some(ufrag) = username_fragment {
candidate.set_ufrag(&ufrag);
}
Ok(candidate)
}
}
#[doc(hidden)]
pub struct NoProtocol(());
#[doc(hidden)]
pub struct HasProtocol<P> {
_marker: PhantomData<P>,
protocol: Protocol,
}
#[doc(hidden)]
pub struct Udp(());
#[doc(hidden)]
pub struct Tcp(());
#[doc(hidden)]
pub struct NoRoute(());
#[doc(hidden)]
pub struct HasRoute {
kind: CandidateKind,
addr: SocketAddr,
base: SocketAddr,
local: SocketAddr,
}
pub struct CandidateBuilder<P, R> {
foundation: Option<String>,
component_id: u16,
prio: Option<u32>,
tcptype: Option<TcpType>,
raddr: Option<SocketAddr>,
ufrag: Option<String>,
local_preference: Option<u32>,
protocol_state: P,
route_state: R,
}
impl CandidateBuilder<NoProtocol, NoRoute> {
pub fn udp(self) -> CandidateBuilder<HasProtocol<Udp>, NoRoute> {
self.into_has_protocol(Protocol::Udp)
}
pub fn tcp(self) -> CandidateBuilder<HasProtocol<Tcp>, NoRoute> {
self.into_has_protocol(Protocol::Tcp)
}
pub fn ssl_tcp(self) -> CandidateBuilder<HasProtocol<Tcp>, NoRoute> {
self.into_has_protocol(Protocol::SslTcp)
}
pub fn tls(self) -> CandidateBuilder<HasProtocol<Tcp>, NoRoute> {
self.into_has_protocol(Protocol::Tls)
}
fn into_has_protocol<NewP>(self, p: Protocol) -> CandidateBuilder<HasProtocol<NewP>, NoRoute> {
CandidateBuilder {
protocol_state: HasProtocol {
_marker: PhantomData,
protocol: p,
},
route_state: self.route_state,
foundation: self.foundation,
component_id: self.component_id,
prio: self.prio,
tcptype: self.tcptype,
raddr: self.raddr,
ufrag: self.ufrag,
local_preference: self.local_preference,
}
}
}
impl<P> CandidateBuilder<HasProtocol<P>, NoRoute> {
pub fn host(self, addr: SocketAddr) -> CandidateBuilder<HasProtocol<P>, HasRoute> {
self.into_has_route(CandidateKind::Host, addr, addr, addr, None)
}
pub fn server_reflexive(
self,
addr: SocketAddr,
base: SocketAddr,
) -> CandidateBuilder<HasProtocol<P>, HasRoute> {
self.into_has_route(
CandidateKind::ServerReflexive,
addr,
base,
base,
Some(Candidate::arbitrary_raddr(addr)),
)
}
pub fn relayed(
self,
addr: SocketAddr,
local: SocketAddr,
) -> CandidateBuilder<HasProtocol<P>, HasRoute> {
self.into_has_route(
CandidateKind::Relayed,
addr,
addr,
local,
Some(Candidate::arbitrary_raddr(addr)),
)
}
fn into_has_route(
self,
kind: CandidateKind,
addr: SocketAddr,
base: SocketAddr,
local: SocketAddr,
raddr: Option<SocketAddr>,
) -> CandidateBuilder<HasProtocol<P>, HasRoute> {
CandidateBuilder {
protocol_state: self.protocol_state,
route_state: HasRoute {
kind,
addr,
base,
local,
},
raddr,
foundation: self.foundation,
component_id: self.component_id,
prio: self.prio,
tcptype: self.tcptype,
ufrag: self.ufrag,
local_preference: self.local_preference,
}
}
}
impl<P> CandidateBuilder<HasProtocol<P>, HasRoute> {
pub fn build(self) -> Result<Candidate, IceError> {
let route = &self.route_state;
if !is_valid_ip(route.addr.ip()) {
return Err(IceError::BadCandidate(format!(
"Invalid IP: {}",
route.addr.ip()
)));
}
if route.addr.is_ipv4() != route.base.is_ipv4() {
return Err(IceError::BadCandidate(format!(
"address IP version mismatch: addr={} base={}",
route.addr.ip(),
route.base.ip(),
)));
}
Ok(Candidate::new(
self.foundation,
self.component_id,
self.protocol_state.protocol,
self.prio,
route.addr,
Some(route.base),
route.kind,
self.raddr,
self.tcptype,
self.ufrag,
route.local,
))
}
}
impl<R> CandidateBuilder<HasProtocol<Tcp>, R> {
pub fn tcptype(mut self, t: TcpType) -> Self {
self.tcptype = Some(t);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_serialize_deserialize() {
let socket_addr = "1.2.3.4:9876".parse().unwrap();
let candidates = [
Candidate::host(socket_addr, Protocol::Udp).unwrap(),
Candidate::host(socket_addr, Protocol::Tcp).unwrap(),
Candidate::builder()
.tcp()
.host(socket_addr)
.tcptype(TcpType::Passive)
.build()
.unwrap(),
];
for c1 in &candidates {
let json = serde_json::to_string(&c1).unwrap();
let c2: Candidate = serde_json::from_str(&json).unwrap();
assert_eq!(c1.proto, c2.proto);
assert_eq!(c1.tcptype, c2.tcptype);
assert_eq!(c1.to_string(), c2.to_string());
}
}
#[test]
fn serialize() {
let socket_addr = "1.2.3.4:9876".parse().unwrap();
let mut candidate = Candidate::host(socket_addr, Protocol::Udp).unwrap();
let json = no_hash(serde_json::to_string(&candidate).unwrap());
let got: serde_json::Value = serde_json::from_str(&json).unwrap();
let expected = serde_json::json!({
"candidate": "candidate:--- 1 udp 2130706175 1.2.3.4 9876 typ host",
"sdpMid": null,
"sdpMLineIndex": 0,
"usernameFragment": null
});
assert_eq!(got, expected);
candidate.ufrag = Some("ufrag".to_string());
let json = no_hash(serde_json::to_string(&candidate).unwrap());
let got: serde_json::Value = serde_json::from_str(&json).unwrap();
let expected = serde_json::json!({
"candidate": "candidate:--- 1 udp 2130706175 1.2.3.4 9876 typ host ufrag ufrag",
"sdpMid": null,
"sdpMLineIndex": 0,
"usernameFragment": "ufrag"
});
assert_eq!(got, expected);
}
fn no_hash(mut s: String) -> String {
let f = s.find("candidate:").unwrap();
let t = s.find(" 1 ").unwrap();
s.replace_range((f + 10)..t, "---");
s
}
#[test]
fn deserialize() {
let json = serde_json::json!({
"candidate": "candidate:12044049749558888150 1 udp 2130706175 1.2.3.4 9876 typ host ufrag ufrag",
"sdpMid": "ignored",
"sdpMLineIndex": 123,
"usernameFragment": "ufrag"
}).to_string();
let candidate: Candidate = serde_json::from_str(&json).unwrap();
assert_eq!(candidate.ufrag(), Some("ufrag"));
assert_eq!(candidate.addr().to_string(), "1.2.3.4:9876");
assert_eq!(candidate.base().to_string(), "1.2.3.4:9876");
assert_eq!(candidate.kind(), CandidateKind::Host);
assert_eq!(candidate.proto(), Protocol::Udp);
assert_eq!(candidate.prio(), 2130706175);
assert_eq!(candidate.component_id(), 1);
assert_eq!(candidate.raddr(), None);
assert!(!candidate.discarded());
}
#[test]
fn to_string() {
let local_addr = "7.8.9.0:2345".parse().unwrap();
let socket_addr = "1.2.3.4:9876".parse().unwrap();
let mut candidate = Candidate::host(socket_addr, Protocol::Udp).unwrap();
assert_eq!(
no_hash(candidate.to_string()),
"candidate:--- 1 udp 2130706175 1.2.3.4 9876 typ host"
);
candidate.ufrag = Some("ufrag".into());
assert_eq!(
no_hash(candidate.to_string()),
"candidate:--- 1 udp 2130706175 1.2.3.4 9876 typ host ufrag ufrag"
);
candidate.raddr = Some("5.5.5.5:5555".parse().unwrap());
assert_eq!(
no_hash(candidate.to_string()),
"candidate:--- 1 udp 2130706175 1.2.3.4 9876 typ host raddr 5.5.5.5 rport 5555 ufrag ufrag"
);
let candidate = Candidate::relayed(socket_addr, local_addr, Protocol::SslTcp).unwrap();
assert_eq!(
no_hash(candidate.to_string()),
"candidate:--- 1 ssltcp 16776959 1.2.3.4 9876 typ relay raddr 0.0.0.0 rport 0"
);
}
#[test]
fn new_from_sdp_string() {
let candidate = Candidate::from_sdp_string(
"candidate:fffeff7e5e895846293d220a 1 udp 2130706175 1.2.3.4 9876 typ host ufrag myuserfrag",
)
.unwrap();
assert_eq!(candidate.ufrag(), Some("myuserfrag"));
assert_eq!(candidate.addr().to_string(), "1.2.3.4:9876");
}
#[test]
fn spoofed_raddr() {
let local_addr = "7.8.9.0:2345".parse().unwrap();
let socket_addr = "1.2.3.4:9876".parse().unwrap();
let base_addr = "5.6.7.8:4321".parse().unwrap();
let host = Candidate::host(socket_addr, Protocol::Udp).unwrap();
assert!(host.raddr().is_none());
let relay = Candidate::relayed(socket_addr, local_addr, Protocol::Udp).unwrap();
assert!(relay.raddr().is_some());
let srflx = Candidate::server_reflexive(socket_addr, base_addr, Protocol::Udp).unwrap();
assert!(srflx.raddr().is_some_and(|raddr| raddr != base_addr));
let prflx = Candidate::peer_reflexive(
Protocol::Udp,
socket_addr,
base_addr,
1000,
None,
"ufrag".into(),
);
assert!(prflx.raddr().is_none());
}
#[test]
fn bad_candidate() {
let s = "candidate:12344 bad value";
assert!(Candidate::from_sdp_string(s).is_err());
}
#[test]
fn tcp_candidates_sanity() {
let socket_addr = "1.2.3.4:9876".parse().unwrap();
let candidates = [
Candidate::host(socket_addr, Protocol::Tcp).unwrap(),
Candidate::builder()
.tcp()
.host(socket_addr)
.build()
.unwrap(),
Candidate::builder()
.tcp()
.host(socket_addr)
.tcptype(TcpType::Passive)
.build()
.unwrap(),
Candidate::builder()
.ssl_tcp()
.host(socket_addr)
.tcptype(TcpType::Active)
.build()
.unwrap(),
Candidate::builder()
.tls()
.host(socket_addr)
.tcptype(TcpType::So)
.build()
.unwrap(),
];
assert!(!candidates[0].to_sdp_string().contains("tcptype"));
assert!(!candidates[1].to_sdp_string().contains("tcptype"));
assert!(candidates[2].to_sdp_string().contains("tcptype passive"));
assert!(candidates[3].to_sdp_string().contains("tcptype active"));
assert!(candidates[4].to_sdp_string().contains("tcptype so"));
}
#[test]
fn lexical_ordering_of_sdp_is_follows_priority() {
let mut candidates = Vec::from([
host("1.1.1.1:0"),
host("2.2.2.2:0"),
srflx("3.3.3.3:0", "4.4.4.4:0"),
srflx("5.5.5.5:0", "6.6.6.6:0"),
relay("8.8.8.8:0", "9.9.9.9:0"),
relay("7.7.7.7:0", "9.9.9.9:0"),
]);
candidates.sort();
assert!(candidates[0].contains("relay"));
assert!(candidates[1].contains("relay"));
assert!(candidates[2].contains("srflx"));
assert!(candidates[3].contains("srflx"));
assert!(candidates[4].contains("host"));
assert!(candidates[5].contains("host"));
}
#[test]
fn srflx_candidate_disallows_mixed_ip_versions() {
let error = Candidate::server_reflexive(
"10.0.0.1:1000".parse().unwrap(),
"[::1]:1000".parse().unwrap(),
"udp",
)
.unwrap_err();
assert_eq!(
error.to_string(),
"ICE bad candidate: addr and base are different IP versions"
);
}
fn host(socket: &str) -> String {
Candidate::host(socket.parse().unwrap(), "udp")
.unwrap()
.to_sdp_string()
}
fn srflx(addr: &str, base: &str) -> String {
Candidate::server_reflexive(addr.parse().unwrap(), base.parse().unwrap(), "udp")
.unwrap()
.to_sdp_string()
}
fn relay(addr: &str, local: &str) -> String {
Candidate::relayed(addr.parse().unwrap(), local.parse().unwrap(), "udp")
.unwrap()
.to_sdp_string()
}
#[test]
fn builder_matches_api_host() {
let addr = "1.2.3.4:1234".parse().unwrap();
let api = Candidate::host(addr, Protocol::Udp).unwrap();
let builder = Candidate::builder().udp().host(addr).build().unwrap();
assert_eq!(api, builder);
let api = Candidate::host(addr, Protocol::Tcp).unwrap();
let builder = Candidate::builder().tcp().host(addr).build().unwrap();
assert_eq!(api, builder);
}
#[test]
fn builder_matches_api_server_reflexive() {
let addr = "95.1.1.1:5000".parse().unwrap();
let base = "192.168.1.50:5000".parse().unwrap();
let api = Candidate::server_reflexive(addr, base, Protocol::Udp).unwrap();
let builder = Candidate::builder()
.udp()
.server_reflexive(addr, base)
.build()
.unwrap();
assert_eq!(api, builder);
}
#[test]
fn builder_matches_api_relayed() {
let addr = "1.2.3.4:3478".parse().unwrap(); let local = "192.168.1.50:5000".parse().unwrap();
let api = Candidate::relayed(addr, local, Protocol::Udp).unwrap();
let builder = Candidate::builder()
.udp()
.relayed(addr, local)
.build()
.unwrap();
assert_eq!(api, builder);
}
#[test]
fn builder_consistency_errors() {
let v4 = "1.2.3.4:1234".parse().unwrap();
let v6 = "[2001:db8::1]:1234".parse().unwrap();
let api_err = Candidate::server_reflexive(v4, v6, Protocol::Udp);
let builder_err = Candidate::builder().udp().server_reflexive(v4, v6).build();
assert!(api_err.is_err());
assert!(builder_err.is_err());
}
}