Skip to main content

zeph_common/
net.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Network utilities shared across crates.
5
6use std::net::IpAddr;
7
8/// Returns `true` if `addr` is a non-routable or private IP address that
9/// should be blocked for outbound connections (SSRF defense).
10///
11/// Covers:
12/// - IPv4: loopback (`127/8`), private (`10/8`, `172.16/12`, `192.168/16`),
13///   link-local (`169.254/16`), unspecified (`0.0.0.0`), broadcast (`255.255.255.255`),
14///   CGNAT (`100.64.0.0/10`, RFC 6598).
15/// - IPv6: loopback (`::1`), unspecified (`::`), ULA (`fc00::/7`),
16///   link-local (`fe80::/10`), IPv4-mapped (`::ffff:x.x.x.x` — applies IPv4 rules).
17#[must_use]
18pub fn is_private_ip(addr: IpAddr) -> bool {
19    match addr {
20        IpAddr::V4(ip) => {
21            let n = u32::from(ip);
22            ip.is_loopback()
23                || ip.is_private()
24                || ip.is_link_local()
25                || ip.is_unspecified()
26                || ip.is_broadcast()
27                // CGNAT range 100.64.0.0/10 (RFC 6598).
28                || (n & 0xFFC0_0000 == 0x6440_0000)
29        }
30        IpAddr::V6(ip) => {
31            ip.is_loopback()
32                || ip.is_unspecified()
33                || ip.to_ipv4_mapped().is_some_and(|v4| {
34                    let n = u32::from(v4);
35                    v4.is_loopback()
36                        || v4.is_private()
37                        || v4.is_link_local()
38                        || v4.is_unspecified()
39                        || v4.is_broadcast()
40                        || (n & 0xFFC0_0000 == 0x6440_0000)
41                })
42                || (ip.segments()[0] & 0xfe00) == 0xfc00 // fc00::/7 unique local
43                || (ip.segments()[0] & 0xffc0) == 0xfe80 // fe80::/10 link-local
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use std::net::{Ipv4Addr, Ipv6Addr};
52
53    #[test]
54    fn loopback_is_private() {
55        assert!(is_private_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)));
56        assert!(is_private_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
57    }
58
59    #[test]
60    fn private_ranges() {
61        assert!(is_private_ip("10.0.0.1".parse().unwrap()));
62        assert!(is_private_ip("172.16.0.1".parse().unwrap()));
63        assert!(is_private_ip("192.168.1.1".parse().unwrap()));
64    }
65
66    #[test]
67    fn link_local() {
68        assert!(is_private_ip("169.254.0.1".parse().unwrap()));
69    }
70
71    #[test]
72    fn unspecified() {
73        assert!(is_private_ip("0.0.0.0".parse().unwrap()));
74        assert!(is_private_ip("::".parse().unwrap()));
75    }
76
77    #[test]
78    fn broadcast() {
79        assert!(is_private_ip("255.255.255.255".parse().unwrap()));
80    }
81
82    #[test]
83    fn cgnat() {
84        assert!(is_private_ip("100.64.0.1".parse().unwrap()));
85        assert!(is_private_ip("100.127.255.255".parse().unwrap()));
86        assert!(!is_private_ip("100.128.0.1".parse().unwrap()));
87    }
88
89    #[test]
90    fn public_ipv4() {
91        assert!(!is_private_ip("8.8.8.8".parse().unwrap()));
92        assert!(!is_private_ip("1.1.1.1".parse().unwrap()));
93        assert!(!is_private_ip("93.184.216.34".parse().unwrap()));
94    }
95
96    #[test]
97    fn ipv6_unique_local() {
98        assert!(is_private_ip("fc00::1".parse().unwrap()));
99        assert!(is_private_ip("fd00::1".parse().unwrap()));
100    }
101
102    #[test]
103    fn ipv6_link_local() {
104        assert!(is_private_ip("fe80::1".parse().unwrap()));
105    }
106
107    #[test]
108    fn ipv6_public() {
109        assert!(!is_private_ip("2001:4860:4860::8888".parse().unwrap()));
110    }
111}