Skip to main content

abuse_contact/
destination.rs

1//! Where the client may connect.
2//!
3//! A registry sends links, and a server sends redirects. Both come from outside the
4//! process, and either can point at an address inside it: a loopback service, a
5//! private network, or a cloud metadata endpoint on `169.254.169.254`. The checks here
6//! stop the client from making such a request for a registry.
7
8use 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
17/// The most redirects the client follows for one request.
18pub(crate) const MAX_REDIRECTS: usize = 5;
19
20/// Which addresses the client may connect to.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum Destinations {
23    /// Public addresses only.
24    ///
25    /// The client refuses an address written in a URL that is not public, and a name
26    /// that resolves to any address that is not public. On a network with NAT64, it
27    /// also reads the IPv4 address inside an IPv6 address, because the connection
28    /// ends there.
29    ///
30    /// The client connects directly and ignores proxy settings from the environment.
31    /// A proxy resolves names where this check cannot see them.
32    #[default]
33    Public,
34
35    /// Any address.
36    ///
37    /// Use it for a registry mirror on your own network, or a test server on
38    /// `127.0.0.1`. Do not use it to read records from registries you do not run.
39    Any,
40}
41
42/// A request the client did not send.
43#[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
64/// Checks a URL before a request goes to it.
65///
66/// A name in the URL is not checked here. It is checked when it resolves, in
67/// [`PublicResolver`], because only then are its addresses known.
68pub(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    // The URL parser writes an IPv6 host in brackets, and writes an IPv4 host in the
85    // dotted form even when the URL spelled it another way, such as `2130706433`.
86    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
100/// Checks a redirect before the client follows it.
101///
102/// `previous` holds the URLs of the request so far, the first one included.
103pub(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    // A redirect from HTTPS to HTTP sends the rest of the request where anybody on the
115    // path can read and change it.
116    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
124/// Returns the redirect policy for a client.
125pub(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/// Resolves names, and refuses a name with an address that is not public.
135///
136/// The client connects to the addresses this returns and no others, so a name cannot
137/// resolve to a public address for the check and a private one for the connection.
138#[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            // The port is a placeholder. The client puts the port of the URL in its place.
147            let found: Vec<SocketAddr> =
148                tokio::net::lookup_host((host.as_str(), 0)).await?.collect();
149
150            // The NAT64 prefix matters only to an IPv6 address, so it is learned only then.
151            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
165/// Learns the NAT64 prefixes of this network, as RFC 7050 describes.
166///
167/// A network without DNS64 answers `ipv4only.arpa` with IPv4 addresses only, and this
168/// gives no prefix.
169async 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
181/// Returns the addresses of a name to connect to, or why the name is refused.
182///
183/// `nat64` holds the NAT64 prefixes of the network, or `None` when they could not be
184/// learned.
185///
186/// One address that is not public refuses the whole name. A name that points at a
187/// public and a private address is a trick to pass a check with one and connect to
188/// the other, and a registry has no reason to publish a private address.
189///
190/// An IPv6 address that the NAT64 gateway could translate, and that cannot be checked
191/// because the prefixes are unknown, is not used. A name left with no address is
192/// refused.
193pub(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            // An IPv6 address that carries an IPv4 one is judged by the IPv4 one, so
205            // the reason names both.
206            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        // The well-known prefix is read by is_public. A prefix this network chose can
223        // only be read when it is known.
224        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
252/// Returns the refusal inside an HTTP error, when the error came from one.
253///
254/// The resolver and the redirect policy report through the HTTP client, which wraps
255/// what they return. The refusal is somewhere down the chain of sources.
256pub(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        // 2130706433 and 0x7f.1 are both 127.0.0.1.
314        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        // DNS64 answers this for a name whose A record is 169.254.169.254.
432        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        // The prefix must be an ordinary public one. Under a documentation prefix such
446        // as 2001:db8::/32 the address is refused before the NAT64 check is reached.
447        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        // The IPv4 address can be checked, so the name still has a usable address.
472        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}