Skip to main content

decdn_common/
net.rs

1//! Host address classification shared by the daemon and the CLI (ADR 001 ยง
2//! Node Discovery).
3//!
4//! [`is_publishable`] decides whether an IP address is reachable from the
5//! public internet. [`route_public_ips`] reports the host's own public
6//! addresses: the source address the kernel picks for its default route, per
7//! address family, kept only when it is publishable. A host with a public IP on
8//! its interface (a typical dedicated or cloud server) reports it; a host
9//! behind NAT (a home connection) reports none, because its route source is a
10//! private address.
11
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
13
14/// Whether `ip` is reachable from the public internet. A stable stand-in for
15/// the unstable `IpAddr::is_global`. An IPv4-mapped IPv6 address is judged as
16/// its IPv4 address.
17#[must_use]
18pub fn is_publishable(ip: IpAddr) -> bool {
19    match ip {
20        IpAddr::V4(v4) => is_publishable_v4(v4),
21        IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
22            Some(v4) => is_publishable_v4(v4),
23            None => is_publishable_v6(v6),
24        },
25    }
26}
27
28const fn is_publishable_v4(ip: Ipv4Addr) -> bool {
29    let [a, b, ..] = ip.octets();
30    !(ip.is_unspecified()
31        || ip.is_loopback()
32        || ip.is_private()
33        || ip.is_link_local()
34        || ip.is_broadcast()
35        || ip.is_documentation()
36        || ip.is_multicast()
37        // 100.64.0.0/10: shared address space (CGNAT).
38        || (a == 100 && (b & 0xC0) == 64)
39        // 198.18.0.0/15: benchmarking.
40        || (a == 198 && (b & 0xFE) == 18)
41        // 0.0.0.0/8 and 240.0.0.0/4: "this network" and reserved.
42        || a == 0
43        || a >= 240)
44}
45
46fn is_publishable_v6(ip: Ipv6Addr) -> bool {
47    let first = ip.segments().first().copied().unwrap_or(0);
48    !(ip.is_unspecified()
49        || ip.is_loopback()
50        || ip.is_multicast()
51        // fc00::/7: unique local.
52        || (first & 0xFE00) == 0xFC00
53        // fe80::/10: link-local unicast.
54        || (first & 0xFFC0) == 0xFE80
55        // 2001:db8::/32: documentation.
56        || (first == 0x2001 && ip.segments().get(1).copied() == Some(0x0DB8)))
57}
58
59/// The host's public IP addresses: for IPv4 and IPv6, the source address of
60/// the default route, kept when [`is_publishable`]. Empty on a host behind NAT
61/// or with no default route.
62///
63/// The probe `connect`s an unbound UDP socket, which only asks the kernel to
64/// pick a route and sends no packet. The targets are documentation addresses,
65/// which follow the default route like any other remote address.
66#[must_use]
67pub fn route_public_ips() -> Vec<IpAddr> {
68    let probes: [(SocketAddr, SocketAddr); 2] = [
69        (
70            SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)),
71            SocketAddr::from((Ipv4Addr::new(192, 0, 2, 1), 9)),
72        ),
73        (
74            SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)),
75            SocketAddr::from((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 9)),
76        ),
77    ];
78    probes
79        .into_iter()
80        .filter_map(|(bind, target)| route_source(bind, target))
81        .filter(|ip| is_publishable(*ip))
82        .collect()
83}
84
85/// The source address the kernel picks to reach `target`, or `None` when the
86/// family has no route.
87fn route_source(bind: SocketAddr, target: SocketAddr) -> Option<IpAddr> {
88    let socket = UdpSocket::bind(bind).ok()?;
89    socket.connect(target).ok()?;
90    socket.local_addr().ok().map(|a| a.ip())
91}
92
93/// The QUIC multiaddr a node at `ip` listening on UDP `port` registers, e.g.
94/// `/ip4/203.0.113.10/udp/4433/quic-v1`.
95#[must_use]
96pub fn quic_multiaddr(ip: IpAddr, port: u16) -> String {
97    match ip {
98        IpAddr::V4(v4) => format!("/ip4/{v4}/udp/{port}/quic-v1"),
99        IpAddr::V6(v6) => format!("/ip6/{v6}/udp/{port}/quic-v1"),
100    }
101}
102
103#[cfg(test)]
104#[allow(clippy::unwrap_used)]
105mod tests;