Skip to main content

minecraft_pinger/common/
ip_filter.rs

1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3/// Predicate applied to resolved IPs before any connection is made.
4pub type IpFilter = dyn Fn(IpAddr) -> bool + Send + Sync;
5
6/// Returns true if the IP belongs to the public internet: excludes private
7/// ranges, loopback, link-local, CGNAT, documentation, benchmarking, multicast
8/// and reserved blocks.
9pub fn is_public_ip(ip: IpAddr) -> bool {
10    match ip {
11        IpAddr::V4(v4) => is_public_ipv4(v4),
12        IpAddr::V6(v6) => is_public_ipv6(v6),
13    }
14}
15
16fn is_public_ipv4(ip: Ipv4Addr) -> bool {
17    let o = ip.octets();
18    !(
19        // 0.0.0.0/8 - "this network"
20        o[0] == 0
21        // 10.0.0.0/8 - private
22        || o[0] == 10
23        // 100.64.0.0/10 - CGNAT / shared address space
24        || (o[0] == 100 && (o[1] & 0b1100_0000) == 64)
25        // 127.0.0.0/8 - loopback
26        || o[0] == 127
27        // 169.254.0.0/16 - link-local (incl. cloud metadata endpoints)
28        || (o[0] == 169 && o[1] == 254)
29        // 172.16.0.0/12 - private
30        || (o[0] == 172 && (o[1] & 0xF0) == 16)
31        // 192.0.0.0/24 - IETF protocol assignments
32        || (o[0] == 192 && o[1] == 0 && o[2] == 0)
33        // 192.0.2.0/24 - TEST-NET-1
34        || (o[0] == 192 && o[1] == 0 && o[2] == 2)
35        // 192.168.0.0/16 - private
36        || (o[0] == 192 && o[1] == 168)
37        // 198.18.0.0/15 - benchmarking
38        || (o[0] == 198 && (o[1] & 0xFE) == 18)
39        // 198.51.100.0/24 - TEST-NET-2
40        || (o[0] == 198 && o[1] == 51 && o[2] == 100)
41        // 203.0.113.0/24 - TEST-NET-3
42        || (o[0] == 203 && o[1] == 0 && o[2] == 113)
43        // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved (incl. broadcast)
44        || o[0] >= 224
45    )
46}
47
48fn is_public_ipv6(ip: Ipv6Addr) -> bool {
49    // ::ffff:x.y.z.w - IPv4-mapped: apply IPv4 rules
50    if let Some(mapped) = ip.to_ipv4_mapped() {
51        return is_public_ipv4(mapped);
52    }
53
54    let s = ip.segments();
55    !(ip.is_loopback()
56        || ip.is_unspecified()
57        // ::/8 - reserved (incl. :: and deprecated IPv4-compatible addresses)
58        || s[0] == 0
59        // fc00::/7 - unique local
60        || (s[0] & 0xfe00) == 0xfc00
61        // fe80::/10 - link-local
62        || (s[0] & 0xffc0) == 0xfe80
63        // ff00::/8 - multicast
64        || (s[0] & 0xff00) == 0xff00
65        // 100::/8 - discard-only / reserved
66        || s[0] == 0x0100
67        // 2001:0000::/24 - IANA special-purpose block (Teredo, benchmarking,
68        // ORCHID...); global allocations start at 2001:0200::
69        || (s[0] == 0x2001 && (s[1] & 0xff00) == 0x0000)
70        // 2001:db8::/32 - documentation
71        || (s[0] == 0x2001 && s[1] == 0x0db8))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use std::net::IpAddr;
78
79    fn allowed(s: &str) -> bool {
80        is_public_ip(s.parse::<IpAddr>().unwrap())
81    }
82
83    #[test]
84    fn rejects_private_and_reserved_ipv4() {
85        assert!(!allowed("127.0.0.1"));
86        assert!(!allowed("10.0.0.1"));
87        assert!(!allowed("172.16.0.1"));
88        assert!(!allowed("172.31.255.255"));
89        assert!(!allowed("192.168.1.1"));
90        assert!(!allowed("169.254.169.254")); // cloud metadata endpoint
91        assert!(!allowed("0.0.0.0"));
92        assert!(!allowed("255.255.255.255"));
93        assert!(!allowed("100.64.0.1")); // CGNAT
94        assert!(!allowed("192.0.2.1")); // TEST-NET-1
95        assert!(!allowed("198.51.100.7")); // TEST-NET-2
96        assert!(!allowed("203.0.113.9")); // TEST-NET-3
97        assert!(!allowed("198.18.0.1")); // benchmarking
98        assert!(!allowed("224.0.0.1")); // multicast
99        assert!(!allowed("240.0.0.1")); // reserved
100        assert!(!allowed("192.0.0.8")); // IETF assignments
101    }
102
103    #[test]
104    fn accepts_public_ipv4() {
105        assert!(allowed("8.8.8.8"));
106        assert!(allowed("1.1.1.1"));
107        assert!(allowed("51.75.20.104"));
108        assert!(allowed("172.32.0.1")); // outside private range
109        assert!(allowed("100.128.0.1")); // outside CGNAT range
110        assert!(allowed("9.9.9.9"));
111    }
112
113    #[test]
114    fn rejects_private_and_reserved_ipv6() {
115        assert!(!allowed("::1"));
116        assert!(!allowed("::"));
117        assert!(!allowed("fe80::1"));
118        assert!(!allowed("fc00::1"));
119        assert!(!allowed("fd12:3456:789a::1"));
120        assert!(!allowed("ff02::1"));
121        assert!(!allowed("2001:db8::1"));
122        assert!(!allowed("2001::1")); // Teredo
123        assert!(!allowed("::ffff:127.0.0.1")); // IPv4-mapped loopback
124        assert!(!allowed("::ffff:10.0.0.1"));
125        assert!(!allowed("::ffff:169.254.169.254"));
126    }
127
128    #[test]
129    fn accepts_public_ipv6() {
130        assert!(allowed("2606:4700:4700::1111"));
131        assert!(allowed("2001:4860:4860::8888"));
132        assert!(allowed("::ffff:8.8.8.8")); // mapped public: IPv4 rules apply
133    }
134
135    #[test]
136    fn test_ipv4_edge_cases() {
137        assert!(!allowed("10.255.255.255"));
138        assert!(allowed("172.15.255.255")); // outside private range (172.16.0.0/12)
139        assert!(!allowed("172.16.0.0")); // inside private range
140        assert!(!allowed("172.31.255.255")); // inside private range
141        assert!(allowed("172.32.0.0")); // outside private range
142        assert!(allowed("192.167.255.255")); // outside private ranges (not 192.168.0.0/16)
143        assert!(allowed("192.169.0.0")); // outside private range
144        assert!(allowed("169.253.255.255")); // outside link-local (169.254.0.0/16)
145        assert!(allowed("169.255.0.0")); // outside link-local
146        assert!(allowed("100.63.255.255")); // outside CGNAT (100.64.0.0/10)
147        assert!(!allowed("100.127.255.255")); // inside CGNAT
148        assert!(allowed("100.128.0.0")); // outside CGNAT
149    }
150
151    #[test]
152    fn test_ipv6_edge_cases() {
153        assert!(!allowed("::ffff:192.168.1.1"));
154        assert!(!allowed("2001:0:0:1::1"));
155        assert!(allowed("2001:200::1"));
156        assert!(!allowed("2001:db8:1::1"));
157    }
158}