1use std::error::Error as StdError;
9use std::fmt;
10use std::net::{IpAddr, Ipv6Addr, SocketAddr};
11
12use ipnet::Ipv6Net;
13
14use reqwest::Url;
15use reqwest::dns::{Addrs, Name, Resolve, Resolving};
16
17pub(crate) const MAX_REDIRECTS: usize = 5;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum Destinations {
23 #[default]
33 Public,
34
35 Any,
40}
41
42#[derive(Debug)]
44pub(crate) struct Refusal {
45 pub(crate) reason: String,
46}
47
48impl fmt::Display for Refusal {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.write_str(&self.reason)
51 }
52}
53
54impl StdError for Refusal {}
55
56impl Refusal {
57 fn new(reason: impl Into<String>) -> Self {
58 Self {
59 reason: reason.into(),
60 }
61 }
62}
63
64pub(crate) fn check_url(url: &Url, destinations: Destinations) -> Result<(), Refusal> {
69 if !matches!(url.scheme(), "http" | "https") {
70 return Err(Refusal::new(format!(
71 "the scheme \"{}\" is not HTTP or HTTPS",
72 url.scheme()
73 )));
74 }
75
76 if destinations == Destinations::Any {
77 return Ok(());
78 }
79
80 let Some(host) = url.host_str() else {
81 return Err(Refusal::new("the URL names no host"));
82 };
83
84 let literal = host
87 .strip_prefix('[')
88 .and_then(|inner| inner.strip_suffix(']'))
89 .unwrap_or(host);
90
91 match literal.parse::<IpAddr>() {
92 Ok(ip) if !crate::is_public(ip) => Err(Refusal::new(format!(
93 "{ip} is a private, reserved or documentation address, and a registry record \
94 must not point inside your network"
95 ))),
96 _ => Ok(()),
97 }
98}
99
100pub(crate) fn check_redirect(
104 url: &Url,
105 previous: &[Url],
106 destinations: Destinations,
107) -> Result<(), Refusal> {
108 if previous.len() > MAX_REDIRECTS {
109 return Err(Refusal::new(format!(
110 "the server sent more than {MAX_REDIRECTS} redirects"
111 )));
112 }
113
114 let from_https = previous.last().is_some_and(|last| last.scheme() == "https");
117 if from_https && url.scheme() == "http" {
118 return Err(Refusal::new("the server redirected from HTTPS to HTTP"));
119 }
120
121 check_url(url, destinations)
122}
123
124pub(crate) fn redirect_policy(destinations: Destinations) -> reqwest::redirect::Policy {
126 reqwest::redirect::Policy::custom(move |attempt| {
127 match check_redirect(attempt.url(), attempt.previous(), destinations) {
128 Ok(()) => attempt.follow(),
129 Err(refusal) => attempt.error(refusal),
130 }
131 })
132}
133
134#[derive(Debug)]
139pub(crate) struct PublicResolver;
140
141impl Resolve for PublicResolver {
142 fn resolve(&self, name: Name) -> Resolving {
143 let host = name.as_str().to_owned();
144
145 Box::pin(async move {
146 let found: Vec<SocketAddr> =
148 tokio::net::lookup_host((host.as_str(), 0)).await?.collect();
149
150 let nat64 = if found.iter().any(SocketAddr::is_ipv6) {
152 discover_nat64().await.ok()
153 } else {
154 Some(Vec::new())
155 };
156
157 match usable_addresses(&host, &found, nat64.as_deref()) {
158 Ok(usable) => Ok(Box::new(usable.into_iter()) as Addrs),
159 Err(refusal) => Err(Box::new(refusal) as Box<dyn StdError + Send + Sync>),
160 }
161 })
162 }
163}
164
165async fn discover_nat64() -> std::io::Result<Vec<Ipv6Net>> {
170 let answer: Vec<Ipv6Addr> = tokio::net::lookup_host((crate::nat64::DISCOVERY_NAME, 0))
171 .await?
172 .filter_map(|address| match address.ip() {
173 IpAddr::V6(v6) => Some(v6),
174 IpAddr::V4(_) => None,
175 })
176 .collect();
177
178 Ok(crate::nat64::prefixes_from_discovery(&answer))
179}
180
181pub(crate) fn usable_addresses(
194 host: &str,
195 found: &[SocketAddr],
196 nat64: Option<&[Ipv6Net]>,
197) -> Result<Vec<SocketAddr>, Refusal> {
198 let mut usable = Vec::new();
199 let mut unchecked = false;
200
201 for &address in found {
202 let ip = address.ip();
203 if !crate::is_public(ip) {
204 let inner = crate::query::unmap(ip);
207 let reason = if inner == ip {
208 format!("{host} resolves to {ip}, which is not a public address")
209 } else {
210 format!(
211 "{host} resolves to {ip}, which is {inner}, and that is not a public address"
212 )
213 };
214 return Err(Refusal::new(reason));
215 }
216
217 let IpAddr::V6(v6) = ip else {
218 usable.push(address);
219 continue;
220 };
221
222 let Some(prefixes) = nat64 else {
225 unchecked = true;
226 continue;
227 };
228 if let Some(v4) = crate::nat64::translations(v6, prefixes)
229 .into_iter()
230 .find(|&v4| !crate::is_public(IpAddr::V4(v4)))
231 {
232 return Err(Refusal::new(format!(
233 "{host} resolves to {ip}, which the NAT64 gateway of this network \
234 translates to {v4}, and that is not a public address"
235 )));
236 }
237 usable.push(address);
238 }
239
240 if !usable.is_empty() {
241 return Ok(usable);
242 }
243 if unchecked {
244 return Err(Refusal::new(format!(
245 "{host} has only IPv6 addresses, and the NAT64 prefix of this network could \
246 not be learned to check them. Check that ipv4only.arpa resolves"
247 )));
248 }
249 Err(Refusal::new(format!("{host} resolves to no address")))
250}
251
252pub(crate) fn refusal_in(error: &reqwest::Error) -> Option<&Refusal> {
257 let mut source = StdError::source(error);
258 while let Some(inner) = source {
259 if let Some(refusal) = inner.downcast_ref::<Refusal>() {
260 return Some(refusal);
261 }
262 source = inner.source();
263 }
264 None
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn url(value: &str) -> Url {
272 Url::parse(value).unwrap()
273 }
274
275 #[test]
276 fn a_public_url_passes() {
277 for value in [
278 "https://rdap.arin.net/registry/ip/8.8.8.8",
279 "http://rdap.cctld.kg/domain/example.kg",
280 "https://8.8.8.8/",
281 "https://[2001:4860:4860::8888]/",
282 ] {
283 assert!(
284 check_url(&url(value), Destinations::Public).is_ok(),
285 "{value}"
286 );
287 }
288 }
289
290 #[test]
291 fn an_address_that_is_not_public_is_refused() {
292 for value in [
293 "http://127.0.0.1/",
294 "http://169.254.169.254/latest/meta-data/",
295 "http://10.0.0.1:8080/",
296 "http://[::1]/",
297 "http://[fe80::1]/",
298 "http://[::ffff:127.0.0.1]/",
299 ] {
300 let refusal = check_url(&url(value), Destinations::Public).unwrap_err();
301 assert!(
302 refusal
303 .reason
304 .contains("private, reserved or documentation"),
305 "{value}: {}",
306 refusal.reason
307 );
308 }
309 }
310
311 #[test]
312 fn an_address_spelled_another_way_is_read_as_the_address() {
313 for value in ["http://2130706433/", "http://0x7f.1/", "http://127.1/"] {
315 assert!(
316 check_url(&url(value), Destinations::Public).is_err(),
317 "{value} must be refused"
318 );
319 }
320 }
321
322 #[test]
323 fn a_scheme_other_than_http_is_refused_whatever_the_destinations() {
324 for destinations in [Destinations::Public, Destinations::Any] {
325 for value in [
326 "file:///etc/passwd",
327 "ftp://example.com/",
328 "gopher://example.com/",
329 ] {
330 let refusal = check_url(&url(value), destinations).unwrap_err();
331 assert!(refusal.reason.contains("is not HTTP or HTTPS"), "{value}");
332 }
333 }
334 }
335
336 #[test]
337 fn a_name_passes_the_url_check_and_is_checked_when_it_resolves() {
338 assert!(check_url(&url("http://localhost/"), Destinations::Public).is_ok());
339 }
340
341 #[test]
342 fn any_allows_an_address_that_is_not_public() {
343 assert!(check_url(&url("http://127.0.0.1:9/"), Destinations::Any).is_ok());
344 }
345
346 #[test]
347 fn a_redirect_to_an_address_that_is_not_public_is_refused() {
348 let previous = [url("https://rdap.example.net/ip/8.8.8.8")];
349
350 assert!(
351 check_redirect(
352 &url("https://169.254.169.254/"),
353 &previous,
354 Destinations::Public
355 )
356 .is_err()
357 );
358 }
359
360 #[test]
361 fn a_redirect_from_https_to_http_is_refused() {
362 let previous = [url("https://rdap.example.net/ip/8.8.8.8")];
363
364 let refusal = check_redirect(
365 &url("http://rdap.example.net/ip/8.8.8.8"),
366 &previous,
367 Destinations::Any,
368 )
369 .unwrap_err();
370
371 assert_eq!(refusal.reason, "the server redirected from HTTPS to HTTP");
372 }
373
374 #[test]
375 fn a_redirect_from_http_to_https_is_followed() {
376 let previous = [url("http://rdap.example.net/ip/8.8.8.8")];
377
378 assert!(
379 check_redirect(
380 &url("https://rdap.example.net/ip/8.8.8.8"),
381 &previous,
382 Destinations::Public
383 )
384 .is_ok()
385 );
386 }
387
388 #[test]
389 fn stops_after_the_redirect_limit() {
390 let hop = url("https://rdap.example.net/");
391 let at_limit = vec![hop.clone(); MAX_REDIRECTS];
392 let past_limit = vec![hop.clone(); MAX_REDIRECTS + 1];
393
394 assert!(check_redirect(&hop, &at_limit, Destinations::Public).is_ok());
395 assert_eq!(
396 check_redirect(&hop, &past_limit, Destinations::Public)
397 .unwrap_err()
398 .reason,
399 "the server sent more than 5 redirects"
400 );
401 }
402
403 fn addresses(values: &[&str]) -> Vec<SocketAddr> {
404 values.iter().map(|value| value.parse().unwrap()).collect()
405 }
406
407 #[test]
408 fn a_name_with_only_public_addresses_is_used_as_it_resolved() {
409 let found = addresses(&["8.8.8.8:0", "[2001:4860::8888]:0"]);
410
411 assert_eq!(
412 usable_addresses("rdap.example.net", &found, Some(&[])).unwrap(),
413 found
414 );
415 }
416
417 #[test]
418 fn one_address_that_is_not_public_refuses_the_whole_name() {
419 let found = addresses(&["8.8.8.8:0", "169.254.169.254:0"]);
420
421 let refusal = usable_addresses("evil.example", &found, Some(&[])).unwrap_err();
422
423 assert_eq!(
424 refusal.reason,
425 "evil.example resolves to 169.254.169.254, which is not a public address"
426 );
427 }
428
429 #[test]
430 fn an_address_under_the_nat64_well_known_prefix_is_refused_without_discovery() {
431 let found = addresses(&["[64:ff9b::a9fe:a9fe]:0"]);
433
434 let refusal = usable_addresses("evil.example", &found, None).unwrap_err();
435
436 assert_eq!(
437 refusal.reason,
438 "evil.example resolves to 64:ff9b::a9fe:a9fe, which is 169.254.169.254, and that \
439 is not a public address"
440 );
441 }
442
443 #[test]
444 fn an_address_under_a_discovered_nat64_prefix_is_judged_by_the_ipv4_address_inside() {
445 let prefixes: [Ipv6Net; 1] = ["2c00:64::/96".parse().unwrap()];
448 assert!(crate::is_public("2c00:64::a9fe:a9fe".parse().unwrap()));
449
450 let refused = usable_addresses(
451 "evil.example",
452 &addresses(&["[2c00:64::a9fe:a9fe]:0"]),
453 Some(&prefixes),
454 )
455 .unwrap_err();
456 assert_eq!(
457 refused.reason,
458 "evil.example resolves to 2c00:64::a9fe:a9fe, which the NAT64 gateway of this \
459 network translates to 169.254.169.254, and that is not a public address"
460 );
461
462 let public = addresses(&["[2c00:64::808:808]:0"]);
463 assert_eq!(
464 usable_addresses("rdap.example.net", &public, Some(&prefixes)).unwrap(),
465 public
466 );
467 }
468
469 #[test]
470 fn an_ipv6_address_is_not_used_when_the_nat64_prefix_cannot_be_learned() {
471 let found = addresses(&["8.8.8.8:0", "[2001:4860::8888]:0"]);
473
474 assert_eq!(
475 usable_addresses("rdap.example.net", &found, None).unwrap(),
476 addresses(&["8.8.8.8:0"])
477 );
478 }
479
480 #[test]
481 fn a_name_with_only_ipv6_addresses_that_cannot_be_checked_is_refused() {
482 let found = addresses(&["[2001:4860::8888]:0"]);
483
484 let refusal = usable_addresses("rdap.example.net", &found, None).unwrap_err();
485
486 assert!(
487 refusal
488 .reason
489 .contains("NAT64 prefix of this network could not be learned"),
490 "{}",
491 refusal.reason
492 );
493 }
494
495 #[test]
496 fn a_name_with_no_address_is_refused() {
497 let refusal = usable_addresses("rdap.example.net", &[], Some(&[])).unwrap_err();
498
499 assert_eq!(refusal.reason, "rdap.example.net resolves to no address");
500 }
501}