use std::net::SocketAddr;
use tracing::warn;
use crate::candidate::PeerCandidates;
use crate::family::Family;
use crate::local::LocalStack;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoCommonFamily {
pub local: Vec<Family>,
pub peer: Vec<Family>,
}
impl std::fmt::Display for NoCommonFamily {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"no common address family: local has {:?}, peer offers {:?}",
self.local, self.peer
)
}
}
impl std::error::Error for NoCommonFamily {}
pub fn dial_order(
local: &LocalStack,
peer: &PeerCandidates,
) -> Result<Vec<SocketAddr>, NoCommonFamily> {
match plan(local, peer) {
DialPlan::Intersection(order) => Ok(order),
DialPlan::FailOpen(order) => {
warn!(
local = ?local.families(),
peer = ?peer.families(),
candidates = order.len(),
"local∩peer address-family intersection is empty; failing OPEN to all peer \
candidates (IPv6-first) — local-stack detection may be a false negative on an \
overlay/split-tunnel/pre-route network"
);
Ok(order)
}
DialPlan::NoCandidates => Err(NoCommonFamily {
local: local.families(),
peer: peer.families().into_iter().collect(),
}),
}
}
#[derive(Debug, PartialEq, Eq)]
enum DialPlan {
Intersection(Vec<SocketAddr>),
FailOpen(Vec<SocketAddr>),
NoCandidates,
}
fn plan(local: &LocalStack, peer: &PeerCandidates) -> DialPlan {
let intersection = candidates_in_preference_order(peer, |family| local.has(family));
if !intersection.is_empty() {
return DialPlan::Intersection(intersection);
}
if peer.is_empty() {
return DialPlan::NoCandidates;
}
DialPlan::FailOpen(candidates_in_preference_order(peer, |_| true))
}
fn candidates_in_preference_order(
peer: &PeerCandidates,
accept: impl Fn(Family) -> bool,
) -> Vec<SocketAddr> {
let mut ordered = Vec::new();
for family in Family::PREFERENCE {
if accept(family) {
ordered.extend(peer.of_family(family));
}
}
ordered
}
#[cfg(test)]
mod tests {
use super::*;
use crate::candidate::CandidateSource;
fn sa(s: &str) -> SocketAddr {
s.parse().unwrap()
}
fn peer_with(v6: bool, v4: bool) -> PeerCandidates {
let mut p = PeerCandidates::new();
if v6 {
p.add(sa("[2001:db8::1]:443"), CandidateSource::Dht);
}
if v4 {
p.add(sa("203.0.113.1:443"), CandidateSource::DnsA);
}
p
}
#[test]
fn empty_intersection_over_a_reachable_peer_fails_open() {
let local = LocalStack::from_flags(false, true);
let peer = peer_with(true, false);
assert_eq!(
plan(&local, &peer),
DialPlan::FailOpen(vec![sa("[2001:db8::1]:443")])
);
assert_eq!(
dial_order(&local, &peer).unwrap(),
vec![sa("[2001:db8::1]:443")]
);
}
#[test]
fn no_default_route_local_dials_all_peer_candidates_ipv6_first() {
let local = LocalStack::from_flags(false, false);
let peer = peer_with(true, true);
assert_eq!(
plan(&local, &peer),
DialPlan::FailOpen(vec![sa("[2001:db8::1]:443"), sa("203.0.113.1:443")])
);
assert_eq!(
dial_order(&local, &peer).unwrap(),
vec![sa("[2001:db8::1]:443"), sa("203.0.113.1:443")]
);
}
#[test]
fn v6_affirmative_local_still_dials_a_v4_only_peer() {
let local = LocalStack::from_flags(true, false);
let peer = peer_with(false, true);
assert_eq!(
plan(&local, &peer),
DialPlan::FailOpen(vec![sa("203.0.113.1:443")])
);
assert_eq!(
dial_order(&local, &peer).unwrap(),
vec![sa("203.0.113.1:443")]
);
}
#[test]
fn affirmative_common_family_uses_the_intersection() {
let local = LocalStack::from_flags(false, true);
let peer = peer_with(true, true);
assert_eq!(
plan(&local, &peer),
DialPlan::Intersection(vec![sa("203.0.113.1:443")])
);
}
#[test]
fn dual_stack_prefers_ipv6() {
let local = LocalStack::from_flags(true, true);
let peer = peer_with(true, true);
let order = dial_order(&local, &peer).unwrap();
assert_eq!(order, vec![sa("[2001:db8::1]:443"), sa("203.0.113.1:443")]);
}
#[test]
fn never_dials_a_family_the_peer_lacks() {
let local = LocalStack::from_flags(true, true);
let peer = peer_with(false, true);
let order = dial_order(&local, &peer).unwrap();
assert_eq!(order, vec![sa("203.0.113.1:443")]);
assert!(order.iter().all(|a| Family::of(a) == Family::V4));
}
#[test]
fn never_dials_a_family_the_local_host_lacks() {
let local = LocalStack::from_flags(false, true);
let peer = peer_with(true, true);
let order = dial_order(&local, &peer).unwrap();
assert_eq!(order, vec![sa("203.0.113.1:443")]);
assert!(order.iter().all(|a| Family::of(a) == Family::V4));
}
#[test]
fn v6_only_local_and_dual_peer_yields_only_v6() {
let local = LocalStack::from_flags(true, false);
let peer = peer_with(true, true);
let order = dial_order(&local, &peer).unwrap();
assert_eq!(order, vec![sa("[2001:db8::1]:443")]);
}
#[test]
fn empty_peer_is_no_common_family() {
let local = LocalStack::from_flags(true, true);
let err = dial_order(&local, &PeerCandidates::new()).unwrap_err();
assert!(err.peer.is_empty());
}
#[test]
fn multiple_addresses_per_family_keep_discovery_order() {
let local = LocalStack::from_flags(true, true);
let mut peer = PeerCandidates::new();
peer.add(sa("[2001:db8::2]:443"), CandidateSource::ListenAddr);
peer.add(sa("198.51.100.7:443"), CandidateSource::Pex);
peer.add(sa("[2001:db8::1]:443"), CandidateSource::Dht);
let order = dial_order(&local, &peer).unwrap();
assert_eq!(
order,
vec![
sa("[2001:db8::2]:443"),
sa("[2001:db8::1]:443"),
sa("198.51.100.7:443"),
]
);
}
}