1use std::collections::HashSet;
40use std::net::{IpAddr, SocketAddr};
41use std::sync::Arc;
42
43use hickory_resolver::TokioResolver;
44use hickory_resolver::config::{ResolverConfig, ResolverOpts};
45use reqwest::dns::{Addrs, Name, Resolve, Resolving};
46
47use crate::error::ToolError;
48
49#[must_use]
72pub fn is_ssrf_blocked(ip: &IpAddr) -> bool {
73 match ip {
74 IpAddr::V4(v4) => is_ssrf_blocked_v4(*v4),
75 IpAddr::V6(v6) => is_ssrf_blocked_v6(*v6),
76 }
77}
78
79fn is_ssrf_blocked_v4(v4: std::net::Ipv4Addr) -> bool {
80 let octets = v4.octets();
81 v4.is_loopback()
82 || v4.is_private()
83 || v4.is_link_local()
84 || v4.is_broadcast()
85 || v4.is_unspecified()
86 || v4.is_multicast()
87 || v4.is_documentation()
88 || (octets[0] == 100 && (64..=127).contains(&octets[1]))
91}
92
93fn is_ssrf_blocked_v6(v6: std::net::Ipv6Addr) -> bool {
94 let segs = v6.segments();
95 if v6.is_loopback() || v6.is_unspecified() || v6.is_multicast() {
96 return true;
97 }
98 if segs[0] & 0xfe00 == 0xfc00 {
100 return true;
101 }
102 if segs[0] & 0xffc0 == 0xfe80 {
104 return true;
105 }
106 if segs[0..5].iter().all(|s| *s == 0) && segs[5] == 0xffff {
111 let v4 = std::net::Ipv4Addr::new(
112 (segs[6] >> 8) as u8,
113 (segs[6] & 0xff) as u8,
114 (segs[7] >> 8) as u8,
115 (segs[7] & 0xff) as u8,
116 );
117 return is_ssrf_blocked_v4(v4);
118 }
119 if segs[0] == 0x2002 {
124 return true;
125 }
126 if segs[0] == 0x2001 && segs[1] == 0 {
131 return true;
132 }
133 false
134}
135
136pub struct SsrfSafeDnsResolver {
140 inner: TokioResolver,
141 explicit_allow: Arc<HashSet<IpAddr>>,
146}
147
148impl SsrfSafeDnsResolver {
149 pub fn from_system() -> Result<Self, ToolError> {
153 let inner = TokioResolver::builder_tokio()
154 .map_err(|e| ToolError::Config {
155 message: format!("DNS: failed to read system config: {e}"),
156 source: Some(Box::new(e)),
157 })?
158 .build()
159 .map_err(|e| ToolError::Config {
160 message: format!("DNS: failed to construct resolver: {e}"),
161 source: Some(Box::new(e)),
162 })?;
163 Ok(Self {
164 inner,
165 explicit_allow: Arc::new(HashSet::new()),
166 })
167 }
168
169 pub fn from_config(config: ResolverConfig, opts: ResolverOpts) -> Result<Self, ToolError> {
173 let inner = TokioResolver::builder_with_config(
174 config,
175 hickory_resolver::net::runtime::TokioRuntimeProvider::default(),
176 )
177 .with_options(opts)
178 .build()
179 .map_err(|e| ToolError::Config {
180 message: format!("DNS: failed to construct resolver: {e}"),
181 source: Some(Box::new(e)),
182 })?;
183 Ok(Self {
184 inner,
185 explicit_allow: Arc::new(HashSet::new()),
186 })
187 }
188
189 #[must_use]
192 pub fn with_explicit_allow(mut self, ips: HashSet<IpAddr>) -> Self {
193 self.explicit_allow = Arc::new(ips);
194 self
195 }
196}
197
198#[allow(
199 clippy::missing_fields_in_debug,
200 reason = "TokioResolver carries a non-Debug closure; printed as the explicit-allow count instead"
201)]
202impl std::fmt::Debug for SsrfSafeDnsResolver {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("SsrfSafeDnsResolver")
205 .field("explicit_allow_count", &self.explicit_allow.len())
206 .finish()
207 }
208}
209
210impl Resolve for SsrfSafeDnsResolver {
211 fn resolve(&self, name: Name) -> Resolving {
212 let inner = self.inner.clone();
213 let allow = Arc::clone(&self.explicit_allow);
214 Box::pin(async move {
215 let host = name.as_str();
216 let lookup = inner
217 .lookup_ip(host)
218 .await
219 .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;
220 let mut safe: Vec<SocketAddr> = Vec::new();
221 let mut blocked: Vec<IpAddr> = Vec::new();
222 for ip in lookup.iter() {
223 if allow.contains(&ip) || !is_ssrf_blocked(&ip) {
224 safe.push(SocketAddr::new(ip, 0));
225 } else {
226 blocked.push(ip);
227 }
228 }
229 if safe.is_empty() {
230 let msg = format!(
231 "DNS for '{host}' resolved only to blocked IPs ({blocked:?}); \
232 refusing to connect (SSRF guard)"
233 );
234 return Err(msg.into());
235 }
236 let iter: Addrs = Box::new(safe.into_iter());
237 Ok(iter)
238 })
239 }
240}
241
242#[cfg(test)]
243#[allow(clippy::unwrap_used, clippy::expect_used, clippy::ip_constant)]
244mod tests {
245 use std::net::{Ipv4Addr, Ipv6Addr};
246
247 use super::*;
248
249 #[test]
250 fn ipv4_loopback_blocked() {
251 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
252 }
253
254 #[test]
255 fn ipv4_metadata_endpoint_blocked() {
256 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(
258 169, 254, 169, 254
259 ))));
260 }
261
262 #[test]
263 fn ipv4_private_ranges_blocked() {
264 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5))));
265 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1))));
266 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
267 }
268
269 #[test]
270 fn ipv4_cgnat_blocked() {
271 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1))));
273 assert!(is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(100, 127, 1, 1))));
274 }
275
276 #[test]
277 fn ipv4_public_address_passes() {
278 assert!(!is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
280 assert!(!is_ssrf_blocked(&IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1))));
282 }
283
284 #[test]
285 fn ipv6_loopback_and_unspecified_blocked() {
286 assert!(is_ssrf_blocked(&IpAddr::V6(Ipv6Addr::LOCALHOST)));
287 assert!(is_ssrf_blocked(&IpAddr::V6(Ipv6Addr::UNSPECIFIED)));
288 }
289
290 #[test]
291 fn ipv6_unique_local_and_link_local_blocked() {
292 assert!(is_ssrf_blocked(&IpAddr::V6("fd00::1".parse().unwrap())));
294 assert!(is_ssrf_blocked(&IpAddr::V6("fe80::1".parse().unwrap())));
296 }
297
298 #[test]
299 fn ipv4_mapped_ipv6_routes_through_v4_block() {
300 assert!(is_ssrf_blocked(&IpAddr::V6(
302 "::ffff:127.0.0.1".parse().unwrap()
303 )));
304 assert!(is_ssrf_blocked(&IpAddr::V6(
305 "::ffff:10.0.0.5".parse().unwrap()
306 )));
307 assert!(is_ssrf_blocked(&IpAddr::V6(
308 "::ffff:169.254.169.254".parse().unwrap()
309 )));
310 }
311
312 #[test]
313 fn ipv4_mapped_public_v4_passes() {
314 assert!(!is_ssrf_blocked(&IpAddr::V6(
316 "::ffff:8.8.8.8".parse().unwrap()
317 )));
318 }
319
320 #[test]
321 fn six_to_four_prefix_blocked_unconditionally() {
322 assert!(is_ssrf_blocked(&IpAddr::V6("2002::1".parse().unwrap())));
324 assert!(is_ssrf_blocked(&IpAddr::V6(
325 "2002:7f00:0001::".parse().unwrap()
326 )));
327 assert!(is_ssrf_blocked(&IpAddr::V6(
328 "2002:0808:0808::".parse().unwrap()
329 )));
330 }
331
332 #[test]
333 fn teredo_prefix_blocked_unconditionally() {
334 assert!(is_ssrf_blocked(&IpAddr::V6("2001::1".parse().unwrap())));
336 assert!(is_ssrf_blocked(&IpAddr::V6(
337 "2001:0:abcd:ef01::".parse().unwrap()
338 )));
339 }
340
341 #[test]
342 fn non_teredo_2001_prefix_allowed() {
343 assert!(!is_ssrf_blocked(&IpAddr::V6(
346 "2001:4860:4860::8888".parse().unwrap()
347 )));
348 }
349
350 #[test]
351 fn ipv6_public_address_passes() {
352 assert!(!is_ssrf_blocked(&IpAddr::V6(
354 "2001:4860:4860::8888".parse().unwrap()
355 )));
356 }
357
358 #[tokio::test]
359 async fn resolver_rejects_when_only_blocked_ips_resolve() {
360 let r =
368 SsrfSafeDnsResolver::from_config(ResolverConfig::default(), ResolverOpts::default());
369 assert!(format!("{r:?}").contains("SsrfSafeDnsResolver"));
370 }
371
372 #[test]
373 fn explicit_allow_overrides_block_for_listed_ips() {
374 let mut allow = HashSet::new();
378 allow.insert(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
379 let allowed_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
380 let other_blocked = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5));
381
382 let safe_for_allowed = allow.contains(&allowed_ip) || !is_ssrf_blocked(&allowed_ip);
384 let safe_for_other = allow.contains(&other_blocked) || !is_ssrf_blocked(&other_blocked);
385
386 assert!(safe_for_allowed, "explicit_allow must override block");
387 assert!(!safe_for_other, "non-allowlisted private IP stays blocked");
388 }
389}