Skip to main content

ebus_discovery/
addr.rs

1//! Address scopes and candidate ordering (record.py:51-61, 93-108; instance.py:41-56).
2
3use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7
8/// The reachability class of an address, declared in candidate preference
9/// order (record.py:51-61): routable addresses before link-local ones.
10///
11/// The classification is python's (record.py:93-108) over python's
12/// `ipaddress` tables of addresses that are not globally reachable, so every
13/// SDK orders the same advertisement the same way. An IPv4-mapped IPv6
14/// address takes its loopback, unspecified, link-local and global tests from
15/// the IPv4 address, as python's does.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub enum AddrScope {
18    /// Globally reachable.
19    Global,
20    /// IPv4 not globally reachable (RFC 1918 and the other special-purpose blocks).
21    Private,
22    /// IPv6 unique local, `fc00::/7`.
23    UniqueLocal,
24    /// Neither global nor private, such as CGNAT `100.64.0.0/10`.
25    Other,
26    /// IPv6 `fe80::/10` or IPv4 `169.254.0.0/16`.
27    LinkLocal,
28    /// Loopback.
29    Loopback,
30    /// The unspecified address.
31    Unspecified,
32}
33
34impl AddrScope {
35    /// The scope of `ip`.
36    pub fn of(ip: IpAddr) -> Self {
37        match ip {
38            IpAddr::V4(v4) => v4_scope(v4),
39            IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
40                Some(v4) => {
41                    if v4.is_loopback() {
42                        Self::Loopback
43                    } else if v4.is_unspecified() {
44                        Self::Unspecified
45                    } else if v4.is_link_local() {
46                        Self::LinkLocal
47                    } else if v4_is_global(v4) {
48                        Self::Global
49                    } else {
50                        Self::Other
51                    }
52                }
53                None => {
54                    if v6.is_loopback() {
55                        Self::Loopback
56                    } else if v6.is_unspecified() {
57                        Self::Unspecified
58                    } else if in_v6(v6, 0xfe80 << 112, 10) {
59                        Self::LinkLocal
60                    } else if in_v6(v6, 0xfc00 << 112, 7) {
61                        Self::UniqueLocal
62                    } else if v6_is_private(v6) {
63                        Self::Other
64                    } else {
65                        Self::Global
66                    }
67                }
68            },
69        }
70    }
71
72    /// Sort key for candidates: lower first. `Private` and `UniqueLocal` tie
73    /// (record.py:51-61).
74    pub const fn preference(self) -> u8 {
75        match self {
76            Self::Global => 0,
77            Self::Private | Self::UniqueLocal => 1,
78            Self::Other => 2,
79            Self::LinkLocal => 3,
80            Self::Loopback => 4,
81            Self::Unspecified => 5,
82        }
83    }
84
85    /// The name python-service-discovery gives the scope (`global`,
86    /// `private`, `unique-local`, `other`, `link-local`, `loopback`,
87    /// `unspecified`).
88    pub const fn as_str(self) -> &'static str {
89        match self {
90            Self::Global => "global",
91            Self::Private => "private",
92            Self::UniqueLocal => "unique-local",
93            Self::Other => "other",
94            Self::LinkLocal => "link-local",
95            Self::Loopback => "loopback",
96            Self::Unspecified => "unspecified",
97        }
98    }
99}
100
101fn v4_scope(v4: Ipv4Addr) -> AddrScope {
102    if v4.is_loopback() {
103        AddrScope::Loopback
104    } else if v4.is_unspecified() {
105        AddrScope::Unspecified
106    } else if v4.is_link_local() {
107        AddrScope::LinkLocal
108    } else if v4_is_private(v4) {
109        AddrScope::Private
110    } else if v4_is_global(v4) {
111        AddrScope::Global
112    } else {
113        AddrScope::Other
114    }
115}
116
117fn in_v4(ip: Ipv4Addr, net: u32, prefix: u32) -> bool {
118    let shift = 32 - prefix;
119    prefix == 0 || (u32::from(ip) >> shift) == (net >> shift)
120}
121
122fn in_v6(ip: Ipv6Addr, net: u128, prefix: u32) -> bool {
123    let shift = 128 - prefix;
124    prefix == 0 || (u128::from(ip) >> shift) == (net >> shift)
125}
126
127const fn v4(a: u8, b: u8, c: u8, d: u8) -> u32 {
128    u32::from_be_bytes([a, b, c, d])
129}
130
131/// Python's `IPv4Address.is_private`: the IANA blocks not globally reachable.
132fn v4_is_private(ip: Ipv4Addr) -> bool {
133    const NETS: [(u32, u32); 14] = [
134        (v4(0, 0, 0, 0), 8),
135        (v4(10, 0, 0, 0), 8),
136        (v4(127, 0, 0, 0), 8),
137        (v4(169, 254, 0, 0), 16),
138        (v4(172, 16, 0, 0), 12),
139        (v4(192, 0, 0, 0), 24),
140        (v4(192, 0, 0, 170), 31),
141        (v4(192, 0, 2, 0), 24),
142        (v4(192, 168, 0, 0), 16),
143        (v4(198, 18, 0, 0), 15),
144        (v4(198, 51, 100, 0), 24),
145        (v4(203, 0, 113, 0), 24),
146        (v4(240, 0, 0, 0), 4),
147        (v4(255, 255, 255, 255), 32),
148    ];
149    const EXCEPT: [(u32, u32); 2] = [(v4(192, 0, 0, 9), 32), (v4(192, 0, 0, 10), 32)];
150    NETS.iter().any(|&(n, p)| in_v4(ip, n, p)) && !EXCEPT.iter().any(|&(n, p)| in_v4(ip, n, p))
151}
152
153/// Python's `IPv4Address.is_global`: not private and not `100.64.0.0/10`.
154fn v4_is_global(ip: Ipv4Addr) -> bool {
155    !in_v4(ip, v4(100, 64, 0, 0), 10) && !v4_is_private(ip)
156}
157
158/// Python's `IPv6Address.is_private` for an address that is not IPv4-mapped.
159fn v6_is_private(ip: Ipv6Addr) -> bool {
160    const NETS: [(u128, u32); 11] = [
161        (1, 128),
162        (0, 128),
163        (0xffff_0000_0000, 96),
164        (0x0064_ff9b_0001 << 80, 48),
165        (0x0100 << 112, 64),
166        (0x2001 << 112, 23),
167        (0x2001_0db8 << 96, 32),
168        (0x2002 << 112, 16),
169        (0x3fff << 112, 20),
170        (0xfc00 << 112, 7),
171        (0xfe80 << 112, 10),
172    ];
173    const EXCEPT: [(u128, u32); 6] = [
174        ((0x2001_0001 << 96) | 1, 128),
175        ((0x2001_0001 << 96) | 2, 128),
176        (0x2001_0003 << 96, 32),
177        (0x2001_0004_0112 << 80, 48),
178        (0x2001_0020 << 96, 28),
179        (0x2001_0030 << 96, 28),
180    ];
181    NETS.iter().any(|&(n, p)| in_v6(ip, n, p)) && !EXCEPT.iter().any(|&(n, p)| in_v6(ip, n, p))
182}
183
184/// An address an mDNS backend reported, with the interface it was seen on.
185///
186/// `Ord` sorts by address, then interface, which breaks ties between
187/// candidates of equal preference.
188#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
189pub struct ScopedAddr {
190    /// The address.
191    pub ip: IpAddr,
192    /// The interface index; required to connect to an IPv6 link-local address.
193    pub ifindex: Option<u32>,
194}
195
196impl ScopedAddr {
197    /// An address with no interface.
198    pub const fn new(ip: IpAddr) -> Self {
199        Self { ip, ifindex: None }
200    }
201
202    /// The address's scope.
203    pub fn scope(&self) -> AddrScope {
204        AddrScope::of(self.ip)
205    }
206
207    /// True when the address can be tried: not loopback or unspecified, and
208    /// an IPv6 link-local address only with an interface (instance.py:41-56).
209    pub fn is_candidate(&self) -> bool {
210        match self.scope() {
211            AddrScope::Loopback | AddrScope::Unspecified => false,
212            AddrScope::LinkLocal => self.ip.is_ipv4() || self.ifindex.is_some(),
213            _ => true,
214        }
215    }
216}
217
218impl From<IpAddr> for ScopedAddr {
219    fn from(ip: IpAddr) -> Self {
220        Self::new(ip)
221    }
222}
223
224/// The addresses worth trying, most preferred first: [`ScopedAddr::is_candidate`]
225/// ones, ordered by [`AddrScope::preference`], then by address, without
226/// duplicates.
227#[cfg(feature = "alloc")]
228pub fn candidate_addresses(addrs: &[ScopedAddr]) -> Vec<ScopedAddr> {
229    let mut out: Vec<ScopedAddr> = addrs
230        .iter()
231        .copied()
232        .filter(ScopedAddr::is_candidate)
233        .collect();
234    out.sort_by_key(|a| (a.scope().preference(), *a));
235    out.dedup();
236    out
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    fn scope(s: &str) -> AddrScope {
244        AddrScope::of(s.parse().unwrap())
245    }
246
247    #[test]
248    fn scopes() {
249        assert_eq!(scope("8.8.8.8"), AddrScope::Global);
250        assert_eq!(scope("192.168.1.2"), AddrScope::Private);
251        assert_eq!(scope("10.0.0.1"), AddrScope::Private);
252        assert_eq!(scope("192.0.0.9"), AddrScope::Global);
253        assert_eq!(scope("100.64.0.1"), AddrScope::Other);
254        assert_eq!(scope("169.254.1.1"), AddrScope::LinkLocal);
255        assert_eq!(scope("127.0.0.1"), AddrScope::Loopback);
256        assert_eq!(scope("0.0.0.0"), AddrScope::Unspecified);
257        assert_eq!(scope("0.1.2.3"), AddrScope::Private);
258        assert_eq!(scope("2606:4700::1"), AddrScope::Global);
259        assert_eq!(scope("fd00::1"), AddrScope::UniqueLocal);
260        assert_eq!(scope("fe80::1"), AddrScope::LinkLocal);
261        assert_eq!(scope("::1"), AddrScope::Loopback);
262        assert_eq!(scope("::"), AddrScope::Unspecified);
263        assert_eq!(scope("2001:db8::1"), AddrScope::Other);
264        assert_eq!(scope("2001:20::1"), AddrScope::Global);
265        assert_eq!(scope("::ffff:10.0.0.1"), AddrScope::Other);
266        assert_eq!(scope("::ffff:8.8.8.8"), AddrScope::Global);
267        assert_eq!(scope("::ffff:127.0.0.1"), AddrScope::Loopback);
268    }
269
270    #[cfg(feature = "alloc")]
271    #[test]
272    fn candidates() {
273        let a = |s: &str, i: Option<u32>| ScopedAddr {
274            ip: s.parse().unwrap(),
275            ifindex: i,
276        };
277        let got = candidate_addresses(&[
278            a("fe80::1", None),
279            a("fe80::2", Some(3)),
280            a("169.254.0.5", None),
281            a("127.0.0.1", None),
282            a("fd00::1", None),
283            a("192.168.1.9", None),
284            a("192.168.1.2", None),
285            a("8.8.8.8", None),
286            a("100.64.0.1", None),
287            a("192.168.1.2", None),
288        ]);
289        assert_eq!(
290            got,
291            [
292                a("8.8.8.8", None),
293                a("192.168.1.2", None),
294                a("192.168.1.9", None),
295                a("fd00::1", None),
296                a("100.64.0.1", None),
297                a("169.254.0.5", None),
298                a("fe80::2", Some(3)),
299            ]
300        );
301    }
302}