1use std::net::{IpAddr, SocketAddr};
7use std::time::Duration;
8
9const RESOLVE_TIMEOUT: Duration = Duration::from_secs(10);
11
12#[must_use]
22pub fn is_private_ip(addr: IpAddr) -> bool {
23 match addr {
24 IpAddr::V4(ip) => {
25 let n = u32::from(ip);
26 ip.is_loopback()
27 || ip.is_private()
28 || ip.is_link_local()
29 || ip.is_unspecified()
30 || ip.is_broadcast()
31 || (n & 0xFFC0_0000 == 0x6440_0000)
33 }
34 IpAddr::V6(ip) => {
35 ip.is_loopback()
36 || ip.is_unspecified()
37 || ip.to_ipv4_mapped().is_some_and(|v4| {
38 let n = u32::from(v4);
39 v4.is_loopback()
40 || v4.is_private()
41 || v4.is_link_local()
42 || v4.is_unspecified()
43 || v4.is_broadcast()
44 || (n & 0xFFC0_0000 == 0x6440_0000)
45 })
46 || (ip.segments()[0] & 0xfe00) == 0xfc00 || (ip.segments()[0] & 0xffc0) == 0xfe80 }
49 }
50}
51
52#[derive(Debug, thiserror::Error)]
57#[non_exhaustive]
58pub enum ResolveError {
59 #[error("DNS resolution timed out after {0:?}")]
61 Timeout(Duration),
62 #[error("DNS resolution failed: {0}")]
64 Lookup(std::io::Error),
65 #[error("SSRF protection: private IP {addr} for host {host}")]
67 PrivateAddress {
68 host: String,
70 addr: IpAddr,
72 },
73}
74
75pub async fn resolve_and_validate(host: &str, port: u16) -> Result<Vec<SocketAddr>, ResolveError> {
102 let addrs: Vec<SocketAddr> =
103 tokio::time::timeout(RESOLVE_TIMEOUT, tokio::net::lookup_host((host, port)))
104 .await
105 .map_err(|_| ResolveError::Timeout(RESOLVE_TIMEOUT))?
106 .map_err(ResolveError::Lookup)?
107 .collect();
108
109 for addr in &addrs {
110 if is_private_ip(addr.ip()) {
111 return Err(ResolveError::PrivateAddress {
112 host: host.to_owned(),
113 addr: addr.ip(),
114 });
115 }
116 }
117
118 Ok(addrs)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::net::{Ipv4Addr, Ipv6Addr};
125
126 #[test]
127 fn loopback_is_private() {
128 assert!(is_private_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)));
129 assert!(is_private_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
130 }
131
132 #[test]
133 fn private_ranges() {
134 assert!(is_private_ip("10.0.0.1".parse().unwrap()));
135 assert!(is_private_ip("172.16.0.1".parse().unwrap()));
136 assert!(is_private_ip("192.168.1.1".parse().unwrap()));
137 }
138
139 #[test]
140 fn link_local() {
141 assert!(is_private_ip("169.254.0.1".parse().unwrap()));
142 }
143
144 #[test]
145 fn unspecified() {
146 assert!(is_private_ip("0.0.0.0".parse().unwrap()));
147 assert!(is_private_ip("::".parse().unwrap()));
148 }
149
150 #[test]
151 fn broadcast() {
152 assert!(is_private_ip("255.255.255.255".parse().unwrap()));
153 }
154
155 #[test]
156 fn cgnat() {
157 assert!(is_private_ip("100.64.0.1".parse().unwrap()));
158 assert!(is_private_ip("100.127.255.255".parse().unwrap()));
159 assert!(!is_private_ip("100.128.0.1".parse().unwrap()));
160 }
161
162 #[test]
163 fn public_ipv4() {
164 assert!(!is_private_ip("8.8.8.8".parse().unwrap()));
165 assert!(!is_private_ip("1.1.1.1".parse().unwrap()));
166 assert!(!is_private_ip("93.184.216.34".parse().unwrap()));
167 }
168
169 #[test]
170 fn ipv6_unique_local() {
171 assert!(is_private_ip("fc00::1".parse().unwrap()));
172 assert!(is_private_ip("fd00::1".parse().unwrap()));
173 }
174
175 #[test]
176 fn ipv6_link_local() {
177 assert!(is_private_ip("fe80::1".parse().unwrap()));
178 }
179
180 #[test]
181 fn ipv6_public() {
182 assert!(!is_private_ip("2001:4860:4860::8888".parse().unwrap()));
183 }
184
185 #[tokio::test]
186 async fn resolve_and_validate_rejects_loopback_hostname() {
187 let err = resolve_and_validate("localhost", 443).await.unwrap_err();
188 assert!(matches!(err, ResolveError::PrivateAddress { .. }));
189 assert!(err.to_string().contains("SSRF protection"));
190 }
191
192 #[tokio::test]
193 async fn resolve_and_validate_rejects_loopback_ip_literal() {
194 let err = resolve_and_validate("127.0.0.1", 443).await.unwrap_err();
195 assert!(matches!(err, ResolveError::PrivateAddress { .. }));
196 }
197}