Skip to main content

dig_ip/
dial.rs

1//! [`dial_order`] — THE local∩peer family-intersection rule, the reason this crate exists.
2//!
3//! Every prior ecosystem copy sorted candidates IPv6-first and raced them, but none removed a family
4//! the LOCAL host cannot reach. [`dial_order`] computes the INTERSECTION of the families the local
5//! host has and the families the peer offers, then emits the peer's addresses of those families in
6//! IPv6-first preference order. Its output is GUARANTEED never to contain a family absent from the
7//! local host — so an IPv4-only host physically cannot emit an IPv6 SYN, and an IPv6-only peer from
8//! an IPv4-only host yields a clean [`NoCommonFamily`] error instead of a doomed attempt that hangs.
9
10use std::net::SocketAddr;
11
12use crate::candidate::PeerCandidates;
13use crate::family::Family;
14use crate::local::LocalStack;
15
16/// The local host and the peer share no reachable address family, so there is no address to dial.
17///
18/// This is a clean, immediate, non-hanging outcome (e.g. an IPv6-only peer from an IPv4-only host):
19/// the caller reports the peer unreachable rather than launching attempts that can only time out.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct NoCommonFamily {
22    /// The families the local host can originate on.
23    pub local: Vec<Family>,
24    /// The families the peer offers candidates on.
25    pub peer: Vec<Family>,
26}
27
28impl std::fmt::Display for NoCommonFamily {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(
31            f,
32            "no common address family: local has {:?}, peer offers {:?}",
33            self.local, self.peer
34        )
35    }
36}
37
38impl std::error::Error for NoCommonFamily {}
39
40/// The addresses to dial, in IPv6-first preference order over the local∩peer family intersection.
41///
42/// For each family the LOCAL host has (IPv6 then IPv4), if the PEER also offers that family, the
43/// peer's addresses of that family are appended in discovery order. The result therefore:
44///
45/// - NEVER contains an address of a family the local host lacks (structural anti-mis-dial guarantee);
46/// - NEVER contains an address of a family the peer lacks (only the peer's own candidates are used);
47/// - lists all viable IPv6 addresses before any IPv4 address (IPv6-first, CLAUDE.md §5.2).
48///
49/// When the intersection is empty the result is [`Err(NoCommonFamily)`] — a clean unreachable, never
50/// an empty attempt list that would silently do nothing.
51pub fn dial_order(
52    local: &LocalStack,
53    peer: &PeerCandidates,
54) -> Result<Vec<SocketAddr>, NoCommonFamily> {
55    let peer_families = peer.families();
56    let mut ordered = Vec::new();
57    for family in local.families() {
58        if peer_families.contains(&family) {
59            ordered.extend(peer.of_family(family));
60        }
61    }
62    if ordered.is_empty() {
63        return Err(NoCommonFamily {
64            local: local.families(),
65            peer: peer_families.into_iter().collect(),
66        });
67    }
68    Ok(ordered)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::candidate::CandidateSource;
75
76    fn sa(s: &str) -> SocketAddr {
77        s.parse().unwrap()
78    }
79
80    /// A peer reachable on the given families (one canned address each).
81    fn peer_with(v6: bool, v4: bool) -> PeerCandidates {
82        let mut p = PeerCandidates::new();
83        if v6 {
84            p.add(sa("[2001:db8::1]:443"), CandidateSource::Dht);
85        }
86        if v4 {
87            p.add(sa("203.0.113.1:443"), CandidateSource::DnsA);
88        }
89        p
90    }
91
92    // (a) v6-only peer, v4-only local host → clean NoCommonFamily, no address emitted, no hang.
93    #[test]
94    fn disjoint_families_report_no_common_family() {
95        let local = LocalStack::from_flags(false, true);
96        let peer = peer_with(true, false);
97        let err = dial_order(&local, &peer).unwrap_err();
98        assert_eq!(err.local, vec![Family::V4]);
99        assert_eq!(err.peer, vec![Family::V6]);
100    }
101
102    // (b) dual-stack both → IPv6 leads the order.
103    #[test]
104    fn dual_stack_prefers_ipv6() {
105        let local = LocalStack::from_flags(true, true);
106        let peer = peer_with(true, true);
107        let order = dial_order(&local, &peer).unwrap();
108        assert_eq!(order, vec![sa("[2001:db8::1]:443"), sa("203.0.113.1:443")]);
109    }
110
111    // (d) NEVER dial a family the PEER lacks: v4-only peer, dual-stack local → only v4.
112    #[test]
113    fn never_dials_a_family_the_peer_lacks() {
114        let local = LocalStack::from_flags(true, true);
115        let peer = peer_with(false, true);
116        let order = dial_order(&local, &peer).unwrap();
117        assert_eq!(order, vec![sa("203.0.113.1:443")]);
118        assert!(order.iter().all(|a| Family::of(a) == Family::V4));
119    }
120
121    // (e) NEVER dial a family the LOCAL host lacks: v4-only local, dual-stack peer → only v4.
122    #[test]
123    fn never_dials_a_family_the_local_host_lacks() {
124        let local = LocalStack::from_flags(false, true);
125        let peer = peer_with(true, true);
126        let order = dial_order(&local, &peer).unwrap();
127        assert_eq!(order, vec![sa("203.0.113.1:443")]);
128        assert!(order.iter().all(|a| Family::of(a) == Family::V4));
129    }
130
131    #[test]
132    fn v6_only_local_and_dual_peer_yields_only_v6() {
133        let local = LocalStack::from_flags(true, false);
134        let peer = peer_with(true, true);
135        let order = dial_order(&local, &peer).unwrap();
136        assert_eq!(order, vec![sa("[2001:db8::1]:443")]);
137    }
138
139    #[test]
140    fn empty_peer_is_no_common_family() {
141        let local = LocalStack::from_flags(true, true);
142        let err = dial_order(&local, &PeerCandidates::new()).unwrap_err();
143        assert!(err.peer.is_empty());
144    }
145
146    #[test]
147    fn multiple_addresses_per_family_keep_discovery_order() {
148        let local = LocalStack::from_flags(true, true);
149        let mut peer = PeerCandidates::new();
150        peer.add(sa("[2001:db8::2]:443"), CandidateSource::ListenAddr);
151        peer.add(sa("198.51.100.7:443"), CandidateSource::Pex);
152        peer.add(sa("[2001:db8::1]:443"), CandidateSource::Dht);
153        let order = dial_order(&local, &peer).unwrap();
154        assert_eq!(
155            order,
156            vec![
157                sa("[2001:db8::2]:443"),
158                sa("[2001:db8::1]:443"),
159                sa("198.51.100.7:443"),
160            ]
161        );
162    }
163}