Skip to main content

agent_fetch/
ip_check.rs

1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3/// Returns `true` if the IP address is private, reserved, loopback, link-local,
4/// or otherwise should not be reachable from an SSRF-safe HTTP client.
5pub fn is_private_ip(ip: IpAddr) -> bool {
6    match ip {
7        IpAddr::V4(v4) => is_private_ipv4(v4),
8        IpAddr::V6(v6) => is_private_ipv6(v6),
9    }
10}
11
12fn is_private_ipv4(ip: Ipv4Addr) -> bool {
13    let octets = ip.octets();
14
15    if ip.is_unspecified() {
16        return true;
17    }
18    if ip.is_loopback() {
19        return true;
20    }
21    // 10.0.0.0/8
22    if octets[0] == 10 {
23        return true;
24    }
25    // 172.16.0.0/12
26    if octets[0] == 172 && (16..=31).contains(&octets[1]) {
27        return true;
28    }
29    // 192.168.0.0/16
30    if octets[0] == 192 && octets[1] == 168 {
31        return true;
32    }
33    // 169.254.0.0/16
34    if octets[0] == 169 && octets[1] == 254 {
35        return true;
36    }
37    if ip.is_broadcast() {
38        return true;
39    }
40    // 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 (documentation)
41    if (octets[0] == 192 && octets[1] == 0 && octets[2] == 2)
42        || (octets[0] == 198 && octets[1] == 51 && octets[2] == 100)
43        || (octets[0] == 203 && octets[1] == 0 && octets[2] == 113)
44    {
45        return true;
46    }
47    // 100.64.0.0/10 (CGNAT)
48    if octets[0] == 100 && (64..=127).contains(&octets[1]) {
49        return true;
50    }
51    // 224.0.0.0/4 (multicast)
52    if octets[0] >= 224 && octets[0] <= 239 {
53        return true;
54    }
55    // 240.0.0.0/4 (reserved)
56    if octets[0] >= 240 {
57        return true;
58    }
59    false
60}
61
62fn is_private_ipv6(ip: Ipv6Addr) -> bool {
63    if ip.is_loopback() {
64        return true;
65    }
66    if ip.is_unspecified() {
67        return true;
68    }
69
70    let segments = ip.segments();
71
72    // fe80::/10
73    if segments[0] & 0xffc0 == 0xfe80 {
74        return true;
75    }
76    // fc00::/7 (ULA — covers fd00:ec2::254 AWS metadata too)
77    if segments[0] & 0xfe00 == 0xfc00 {
78        return true;
79    }
80    // ff00::/8 (multicast)
81    if segments[0] & 0xff00 == 0xff00 {
82        return true;
83    }
84
85    if let Some(v4) = ip.to_ipv4_mapped() {
86        return is_private_ipv4(v4);
87    }
88
89    false
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn loopback_v4() {
98        assert!(is_private_ip("127.0.0.1".parse().unwrap()));
99        assert!(is_private_ip("127.255.255.255".parse().unwrap()));
100    }
101
102    #[test]
103    fn private_ranges_v4() {
104        assert!(is_private_ip("10.0.0.1".parse().unwrap()));
105        assert!(is_private_ip("10.255.255.255".parse().unwrap()));
106        assert!(is_private_ip("172.16.0.1".parse().unwrap()));
107        assert!(is_private_ip("172.31.255.255".parse().unwrap()));
108        assert!(is_private_ip("192.168.0.1".parse().unwrap()));
109        assert!(is_private_ip("192.168.255.255".parse().unwrap()));
110    }
111
112    #[test]
113    fn link_local_v4() {
114        assert!(is_private_ip("169.254.0.1".parse().unwrap()));
115        assert!(is_private_ip("169.254.169.254".parse().unwrap())); // cloud metadata
116    }
117
118    #[test]
119    fn unspecified_and_broadcast() {
120        assert!(is_private_ip("0.0.0.0".parse().unwrap()));
121        assert!(is_private_ip("255.255.255.255".parse().unwrap()));
122    }
123
124    #[test]
125    fn multicast_v4() {
126        assert!(is_private_ip("224.0.0.1".parse().unwrap()));
127        assert!(is_private_ip("239.255.255.255".parse().unwrap()));
128    }
129
130    #[test]
131    fn reserved_v4() {
132        assert!(is_private_ip("240.0.0.1".parse().unwrap()));
133        assert!(is_private_ip("100.64.0.1".parse().unwrap()));
134    }
135
136    #[test]
137    fn public_v4_allowed() {
138        assert!(!is_private_ip("8.8.8.8".parse().unwrap()));
139        assert!(!is_private_ip("1.1.1.1".parse().unwrap()));
140        assert!(!is_private_ip("93.184.216.34".parse().unwrap()));
141    }
142
143    #[test]
144    fn loopback_v6() {
145        assert!(is_private_ip("::1".parse().unwrap()));
146    }
147
148    #[test]
149    fn unspecified_v6() {
150        assert!(is_private_ip("::".parse().unwrap()));
151    }
152
153    #[test]
154    fn link_local_v6() {
155        assert!(is_private_ip("fe80::1".parse().unwrap()));
156    }
157
158    #[test]
159    fn unique_local_v6() {
160        assert!(is_private_ip("fc00::1".parse().unwrap()));
161        assert!(is_private_ip("fd00::1".parse().unwrap()));
162    }
163
164    #[test]
165    fn ec2_metadata_v6() {
166        assert!(is_private_ip("fd00:ec2::254".parse().unwrap()));
167    }
168
169    #[test]
170    fn multicast_v6() {
171        assert!(is_private_ip("ff02::1".parse().unwrap()));
172    }
173
174    #[test]
175    fn ipv4_mapped_v6_private() {
176        assert!(is_private_ip("::ffff:127.0.0.1".parse().unwrap()));
177        assert!(is_private_ip("::ffff:10.0.0.1".parse().unwrap()));
178        assert!(is_private_ip("::ffff:192.168.1.1".parse().unwrap()));
179        assert!(is_private_ip("::ffff:169.254.169.254".parse().unwrap()));
180    }
181
182    #[test]
183    fn ipv4_mapped_v6_public() {
184        assert!(!is_private_ip("::ffff:8.8.8.8".parse().unwrap()));
185    }
186
187    #[test]
188    fn public_v6_allowed() {
189        assert!(!is_private_ip("2607:f8b0:4004:800::200e".parse().unwrap()));
190    }
191}