Skip to main content

rightkit_http/
netpolicy.rs

1//! Outbound address policy: address classification and the SSRF guard behind every
2//! [`crate::Client`] and [`crate::AsyncClient`] request.
3//!
4//! The classification mirrors `rightkit-browser`'s `policy.rs` (`classify_ip`,
5//! `AddressClass`) with the same semantics, so a browser page and an HTTP call reach the
6//! same verdict for the same address. A test cross-checks the two tables.
7//!
8//! How the guard works:
9//!
10//! 1. The host of the request URL (and of every redirect hop) is resolved once, by this
11//!    crate's resolver. Each resolved address is classified. One denied address denies the
12//!    whole host.
13//! 2. Only the vetted addresses are handed to the connector, so the socket opens to an
14//!    address that was checked. There is no second lookup between check and connect.
15//! 3. Names that resolve to no address are denied.
16//!
17//! Default (public-web) policy denies loopback, RFC 1918, link-local (including
18//! `169.254.169.254`), CGNAT `100.64/10`, multicast, reserved, unspecified and broadcast
19//! addresses, IPv6 ULA `fc00::/7` and link-local `fe80::/10`, and IPv6 forms that embed an
20//! IPv4 address (mapped, compatible, NAT64 `64:ff9b::/96`, 6to4 `2002::/16`) by the class of
21//! the embedded IPv4 address. Link-local and cloud-metadata destinations cannot be allowed
22//! by any hook.
23
24use std::error::Error;
25use std::fmt;
26use std::io;
27use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
28use std::str::FromStr;
29use std::sync::mpsc;
30use std::sync::Arc;
31use std::time::Duration;
32
33use crate::error::{HttpError, Phase};
34
35/// Address class of a destination. Everything except `Public` is denied by default.
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
37pub enum AddressClass {
38    Public,
39    Loopback,
40    /// `169.254/16`, `fe80::/10`; includes the cloud metadata endpoint.
41    LinkLocal,
42    /// RFC 1918, CGNAT `100.64/10`, ULA `fc00::/7`, site-local `fec0::/10`.
43    Private,
44    /// IETF protocol assignments, documentation, benchmarking, `240/4`, broadcast,
45    /// `2001:db8::/32`, Teredo.
46    Reserved,
47    Unspecified,
48    Multicast,
49}
50
51/// Classify one address. IPv6 forms that embed an IPv4 address take the embedded class.
52pub fn classify_ip(ip: IpAddr) -> AddressClass {
53    match ip {
54        IpAddr::V4(v4) => classify_v4(v4),
55        IpAddr::V6(v6) => classify_v6(v6),
56    }
57}
58
59fn classify_v4(ip: Ipv4Addr) -> AddressClass {
60    let [a, b, c, _] = ip.octets();
61    if a == 127 {
62        AddressClass::Loopback
63    } else if a == 0 {
64        AddressClass::Unspecified
65    } else if a == 169 && b == 254 {
66        AddressClass::LinkLocal
67    } else if a == 10
68        || (a == 172 && (16..=31).contains(&b))
69        || (a == 192 && b == 168)
70        || (a == 100 && (64..=127).contains(&b))
71    {
72        AddressClass::Private
73    } else if (224..=239).contains(&a) {
74        AddressClass::Multicast
75    } else if a >= 240
76        || (a == 192 && b == 0 && c == 0)
77        || (a == 192 && b == 0 && c == 2)
78        || (a == 198 && (b == 18 || b == 19))
79        || (a == 198 && b == 51 && c == 100)
80        || (a == 203 && b == 0 && c == 113)
81    {
82        AddressClass::Reserved
83    } else {
84        AddressClass::Public
85    }
86}
87
88fn v4_from_segments(hi: u16, lo: u16) -> Ipv4Addr {
89    Ipv4Addr::new((hi >> 8) as u8, hi as u8, (lo >> 8) as u8, lo as u8)
90}
91
92fn v4_from_octets(o: &[u8; 16], idx: [usize; 4]) -> Ipv4Addr {
93    Ipv4Addr::new(o[idx[0]], o[idx[1]], o[idx[2]], o[idx[3]])
94}
95
96/// Every IPv4 address an IPv6 address can embed: IPv4-mapped `::ffff:a.b.c.d`, the
97/// deprecated IPv4-compatible `::a.b.c.d`, NAT64 `64:ff9b::/96`, 6to4 `2002::/16`, and the
98/// local-use NAT64 space `64:ff9b:1::/48` (RFC 8215). The local-use prefix may be deployed
99/// at several lengths (RFC 6052 section 2.2), so each of the /48, /56, /64 and /96 layouts
100/// is returned. `::`, `::1` and non-embedding addresses return nothing.
101pub fn embedded_v4s(ip: IpAddr) -> Vec<Ipv4Addr> {
102    let IpAddr::V6(v6) = ip else {
103        return Vec::new();
104    };
105    if v6.is_loopback() || v6.is_unspecified() {
106        return Vec::new();
107    }
108    let s = v6.segments();
109    if let Some(v4) = v6.to_ipv4_mapped() {
110        return vec![v4];
111    }
112    if s[..6] == [0, 0, 0, 0, 0, 0] || s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
113        return vec![v4_from_segments(s[6], s[7])];
114    }
115    if s[0] == 0x2002 {
116        return vec![v4_from_segments(s[1], s[2])];
117    }
118    if s[0] == 0x64 && s[1] == 0xff9b && s[2] == 1 {
119        let o = v6.octets();
120        return vec![
121            v4_from_octets(&o, [12, 13, 14, 15]),
122            v4_from_octets(&o, [9, 10, 11, 12]),
123            v4_from_octets(&o, [7, 9, 10, 11]),
124            v4_from_octets(&o, [6, 7, 9, 10]),
125        ];
126    }
127    Vec::new()
128}
129
130/// The IPv4 address embedded in an IPv6 address (see [`embedded_v4s`]): the first
131/// non-public one when several layouts are possible, otherwise the first.
132pub fn embedded_v4(ip: IpAddr) -> Option<Ipv4Addr> {
133    let all = embedded_v4s(ip);
134    all.iter()
135        .copied()
136        .find(|v4| classify_v4(*v4) != AddressClass::Public)
137        .or_else(|| all.first().copied())
138}
139
140fn classify_v6(v6: Ipv6Addr) -> AddressClass {
141    let s = v6.segments();
142    if v6.is_loopback() {
143        return AddressClass::Loopback;
144    }
145    if v6.is_unspecified() {
146        return AddressClass::Unspecified;
147    }
148    let embedded = embedded_v4s(IpAddr::V6(v6));
149    if !embedded.is_empty() {
150        return embedded
151            .into_iter()
152            .map(classify_v4)
153            .find(|c| *c != AddressClass::Public)
154            .unwrap_or(AddressClass::Public);
155    }
156    if v6.is_multicast() {
157        AddressClass::Multicast
158    } else if s[0] & 0xffc0 == 0xfe80 {
159        AddressClass::LinkLocal
160    } else if s[0] & 0xfe00 == 0xfc00 || s[0] & 0xffc0 == 0xfec0 {
161        AddressClass::Private
162    } else if (s[0] == 0x2001 && s[1] == 0x0db8) || (s[0] == 0x2001 && s[1] == 0) {
163        AddressClass::Reserved
164    } else {
165        AddressClass::Public
166    }
167}
168
169/// Addresses no hook may allow: link-local (which holds `169.254.169.254`), AWS's IPv6
170/// infrastructure block `fd00:ec2::/32` (metadata `fd00:ec2::254`, DNS `fd00:ec2::253`),
171/// and the metadata addresses outside link-local: Alibaba `100.100.100.200`, Azure's
172/// wire server `168.63.129.16`, and Oracle/other `192.0.0.192`. IPv6 forms embedding one
173/// of these are covered too. Returns why the address is blocked.
174fn hard_block(ip: IpAddr) -> Option<&'static str> {
175    fn one(ip: IpAddr) -> Option<&'static str> {
176        if classify_ip(ip) == AddressClass::LinkLocal {
177            return Some("is link-local (cloud metadata range)");
178        }
179        match ip {
180            IpAddr::V4(v4)
181                if [[100, 100, 100, 200], [168, 63, 129, 16], [192, 0, 0, 192]]
182                    .contains(&v4.octets()) =>
183            {
184                Some("is a cloud metadata address")
185            }
186            IpAddr::V6(v6) if v6.segments()[0] == 0xfd00 && v6.segments()[1] == 0x0ec2 => {
187                Some("is in the AWS IPv6 metadata block fd00:ec2::/32")
188            }
189            _ => None,
190        }
191    }
192    one(ip).or_else(|| {
193        embedded_v4s(ip)
194            .into_iter()
195            .find_map(|v4| one(IpAddr::V4(v4)))
196    })
197}
198
199/// How a host string spelled an IP address.
200#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
201pub enum HostIpKind {
202    /// Four decimal parts without leading zeros: `1.1.1.1`.
203    Ipv4DottedQuad,
204    /// Any other IPv4 spelling `inet_aton` accepts: `127.1`, `0177.0.0.1`, `0x7f.0.0.1`,
205    /// `2130706433`, `10.1.2`, and the dotted quad with a trailing dot.
206    Ipv4Legacy,
207    /// An IPv6 literal, with or without brackets, including IPv4-mapped forms.
208    Ipv6,
209}
210
211/// A host string that is an IP address, as parsed by [`parse_host_ip`].
212#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
213pub struct ParsedHostIp {
214    pub addr: IpAddr,
215    pub kind: HostIpKind,
216    /// True for a dotted quad or an IPv6 literal, which every parser reads the same way.
217    /// False for [`HostIpKind::Ipv4Legacy`] spellings, which downstream parsers disagree on.
218    pub is_canonical: bool,
219}
220
221/// Parse a host string as an IP address the way `inet_aton` and system resolvers do, and say
222/// how it was spelled. The input is a host as it appears in a URL: ASCII case does not
223/// matter, IPv6 may be bracketed, and one trailing dot is allowed (and makes an IPv4
224/// spelling non-canonical).
225///
226/// Accepted, with the result:
227///
228/// | Input | `addr` | `kind` | `is_canonical` |
229/// |---|---|---|---|
230/// | `1.1.1.1` | 1.1.1.1 | `Ipv4DottedQuad` | true |
231/// | `127.1` | 127.0.0.1 | `Ipv4Legacy` | false |
232/// | `10.1.2` | 10.1.0.2 | `Ipv4Legacy` | false |
233/// | `0177.0.0.1` (octal parts) | 127.0.0.1 | `Ipv4Legacy` | false |
234/// | `0x7f.0.0.1`, `0x7f000001` (hex) | 127.0.0.1 | `Ipv4Legacy` | false |
235/// | `2130706433` (one 32-bit part) | 127.0.0.1 | `Ipv4Legacy` | false |
236/// | `1.1.1.1.` (trailing dot) | 1.1.1.1 | `Ipv4Legacy` | false |
237/// | `::1`, `[::1]`, `::ffff:127.0.0.1` | as written | `Ipv6` | true |
238///
239/// `inet_aton` rules: one to four parts; each part is decimal, octal (a leading `0`) or hex
240/// (`0x`); every part but the last is at most 255; the last part fills the remaining bytes
241/// (so `a.b.c` has a 16-bit last part and `a.b` a 24-bit one). Returns `None` for hostnames,
242/// for more than four parts, for out-of-range parts, for a digit outside the part's radix
243/// (`08.1`), and for empty parts.
244pub fn parse_host_ip(host: &str) -> Option<ParsedHostIp> {
245    let trimmed = host.trim_matches(['[', ']']);
246    if let Ok(ip) = trimmed.parse::<IpAddr>() {
247        let (kind, is_canonical) = match ip {
248            IpAddr::V4(_) => (HostIpKind::Ipv4DottedQuad, true),
249            IpAddr::V6(_) => (HostIpKind::Ipv6, true),
250        };
251        return Some(ParsedHostIp {
252            addr: ip,
253            kind,
254            is_canonical,
255        });
256    }
257    let (body, trailing_dot) = match trimmed.strip_suffix('.') {
258        Some(b) => (b, true),
259        None => (trimmed, false),
260    };
261    let v4 = parse_legacy_ipv4(body)?;
262    let canonical = !trailing_dot && body.eq_ignore_ascii_case(&v4.to_string());
263    Some(ParsedHostIp {
264        addr: IpAddr::V4(v4),
265        kind: if canonical {
266            HostIpKind::Ipv4DottedQuad
267        } else {
268            HostIpKind::Ipv4Legacy
269        },
270        is_canonical: canonical,
271    })
272}
273
274/// Parse an IPv4 address with the `inet_aton` rules described at [`parse_host_ip`]. This
275/// accepts every spelling, canonical or not, and does not accept a trailing dot, brackets
276/// or IPv6; use [`parse_host_ip`] when the spelling matters.
277pub fn parse_legacy_ipv4(host: &str) -> Option<Ipv4Addr> {
278    let parts: Vec<&str> = host.split('.').collect();
279    if parts.is_empty() || parts.len() > 4 {
280        return None;
281    }
282    let mut values = Vec::with_capacity(parts.len());
283    for part in &parts {
284        let (digits, radix) =
285            if let Some(hex) = part.strip_prefix("0x").or_else(|| part.strip_prefix("0X")) {
286                (hex, 16)
287            } else if part.len() > 1 && part.starts_with('0') {
288                (&part[1..], 8)
289            } else {
290                (*part, 10)
291            };
292        if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
293            return None;
294        }
295        values.push(u64::from_str_radix(digits, radix).ok()?);
296    }
297    let (last, init) = values.split_last()?;
298    if init.iter().any(|v| *v > 255) {
299        return None;
300    }
301    let tail_bytes = 4 - init.len();
302    if *last >= 1u64 << (8 * tail_bytes) {
303        return None;
304    }
305    let mut addr: u32 = 0;
306    for v in init {
307        addr = (addr << 8) | *v as u32;
308    }
309    addr = addr.checked_shl(8 * tail_bytes as u32).unwrap_or(0) | *last as u32;
310    Some(Ipv4Addr::from(addr))
311}
312
313/// Hostnames that always mean "cloud instance metadata".
314fn is_metadata_host(host: &str) -> bool {
315    matches!(
316        host,
317        "metadata" | "metadata.google.internal" | "metadata.goog" | "instance-data"
318    ) || host.ends_with(".metadata.google.internal")
319}
320
321/// Lowercase, strip IPv6 brackets and one trailing dot.
322pub(crate) fn normalize_host(host: &str) -> String {
323    let h = host.trim_matches(['[', ']']).to_ascii_lowercase();
324    h.strip_suffix('.').map(str::to_string).unwrap_or(h)
325}
326
327/// A CIDR range such as `10.0.0.0/8` or `fd00::/8`.
328#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
329pub struct IpNet {
330    addr: IpAddr,
331    prefix: u8,
332}
333
334/// `IpNet` text or prefix length was invalid.
335#[derive(Clone, Debug, Eq, PartialEq)]
336pub struct ParseIpNetError(pub String);
337
338impl fmt::Display for ParseIpNetError {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        write!(f, "invalid CIDR range: {}", self.0)
341    }
342}
343impl Error for ParseIpNetError {}
344
345impl IpNet {
346    /// `prefix` is bits of the network part: at most 32 for IPv4, 128 for IPv6.
347    pub fn new(addr: IpAddr, prefix: u8) -> Result<Self, ParseIpNetError> {
348        let max = if addr.is_ipv4() { 32 } else { 128 };
349        if prefix > max {
350            return Err(ParseIpNetError(format!("prefix {prefix} exceeds {max}")));
351        }
352        Ok(Self { addr, prefix })
353    }
354
355    pub fn addr(&self) -> IpAddr {
356        self.addr
357    }
358
359    pub fn prefix(&self) -> u8 {
360        self.prefix
361    }
362
363    /// True when `ip` is in the range. Families must match; use [`embedded_v4`] first to
364    /// test an IPv6 form against an IPv4 range.
365    pub fn contains(&self, ip: &IpAddr) -> bool {
366        match (self.addr, ip) {
367            (IpAddr::V4(net), IpAddr::V4(ip)) => {
368                let mask = if self.prefix == 0 {
369                    0
370                } else {
371                    u32::MAX << (32 - u32::from(self.prefix))
372                };
373                u32::from(net) & mask == u32::from(*ip) & mask
374            }
375            (IpAddr::V6(net), IpAddr::V6(ip)) => {
376                let mask = if self.prefix == 0 {
377                    0
378                } else {
379                    u128::MAX << (128 - u32::from(self.prefix))
380                };
381                u128::from(net) & mask == u128::from(*ip) & mask
382            }
383            _ => false,
384        }
385    }
386}
387
388impl FromStr for IpNet {
389    type Err = ParseIpNetError;
390
391    /// `a.b.c.d/len`, `x::/len`, or a bare address (a single-host range).
392    fn from_str(s: &str) -> Result<Self, Self::Err> {
393        let (addr, prefix) = match s.split_once('/') {
394            Some((a, p)) => (a, Some(p)),
395            None => (s, None),
396        };
397        let addr: IpAddr = addr
398            .trim()
399            .parse()
400            .map_err(|_| ParseIpNetError(s.to_string()))?;
401        let prefix = match prefix {
402            Some(p) => p
403                .trim()
404                .parse::<u8>()
405                .map_err(|_| ParseIpNetError(s.to_string()))?,
406            None => {
407                if addr.is_ipv4() {
408                    32
409                } else {
410                    128
411                }
412            }
413        };
414        Self::new(addr, prefix)
415    }
416}
417
418impl fmt::Display for IpNet {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        write!(f, "{}/{}", self.addr, self.prefix)
421    }
422}
423
424/// Why an address or host was refused. Converts to [`crate::HttpError::AddressDenied`].
425#[derive(Clone, Debug, Eq, PartialEq)]
426pub struct AddressDenial {
427    pub host: String,
428    /// The offending address; `None` when the denial is about the name or scheme.
429    pub addr: Option<IpAddr>,
430    pub class: Option<AddressClass>,
431    pub reason: String,
432}
433
434impl fmt::Display for AddressDenial {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        write!(
437            f,
438            "address denied for host '{}': {}",
439            self.host, self.reason
440        )
441    }
442}
443impl Error for AddressDenial {}
444
445type DenyHook = Arc<dyn Fn(&IpAddr) -> Option<String> + Send + Sync>;
446type AllowHook = Arc<dyn Fn(&IpAddr) -> bool + Send + Sync>;
447type ResolverHook = Arc<dyn Fn(&str) -> Vec<IpAddr> + Send + Sync>;
448
449#[derive(Clone)]
450enum Mode {
451    PublicWeb,
452    FirstParty(Vec<String>),
453}
454
455#[derive(Clone)]
456struct AllowRule {
457    hosts: Vec<String>,
458    pred: AllowHook,
459}
460
461/// Outbound address policy carried by [`crate::ClientConfig::address_policy`].
462///
463/// The default is public-web policy: only public addresses are reachable. Callers add
464/// extra denials with [`AddressPolicy::deny_ranges`] and [`AddressPolicy::deny_if`], and
465/// open specific non-public destinations with [`AddressPolicy::allow_if`]. Custom denials
466/// always win over allowances.
467#[derive(Clone)]
468pub struct AddressPolicy {
469    mode: Mode,
470    deny_ranges: Vec<IpNet>,
471    deny_if: Vec<DenyHook>,
472    allow: Vec<AllowRule>,
473    /// First-party hosts allowed to resolve to non-public addresses.
474    private_ok: Vec<String>,
475    /// Follow an HTTPS to HTTP redirect (refused by default).
476    allow_downgrade: bool,
477    /// Accept non-canonical IPv4 host spellings (refused by default).
478    allow_legacy_ipv4: bool,
479    resolver: Option<ResolverHook>,
480}
481
482impl Default for AddressPolicy {
483    fn default() -> Self {
484        Self::public_web()
485    }
486}
487
488impl fmt::Debug for AddressPolicy {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        let mut d = f.debug_struct("AddressPolicy");
491        match &self.mode {
492            Mode::PublicWeb => d.field("mode", &"public-web"),
493            Mode::FirstParty(hosts) => d.field("mode", &format_args!("first-party {hosts:?}")),
494        };
495        d.field("deny_ranges", &self.deny_ranges)
496            .field("deny_if", &self.deny_if.len())
497            .field(
498                "allow_if_hosts",
499                &self
500                    .allow
501                    .iter()
502                    .flat_map(|r| r.hosts.iter())
503                    .collect::<Vec<_>>(),
504            )
505            .field("private_resolution_hosts", &self.private_ok)
506            .field("allow_insecure_redirects", &self.allow_downgrade)
507            .field("allow_legacy_ipv4", &self.allow_legacy_ipv4)
508            .field("custom_resolver", &self.resolver.is_some())
509            .finish()
510    }
511}
512
513impl AddressPolicy {
514    /// Public-web policy: only public addresses are reachable. This is the default.
515    pub fn public_web() -> Self {
516        Self {
517            mode: Mode::PublicWeb,
518            deny_ranges: Vec::new(),
519            deny_if: Vec::new(),
520            allow: Vec::new(),
521            private_ok: Vec::new(),
522            allow_downgrade: false,
523            allow_legacy_ipv4: false,
524            resolver: None,
525        }
526    }
527
528    /// First-party policy: only the listed hosts (exact match, ASCII case-insensitive, one
529    /// trailing dot ignored), only over HTTPS, on any redirect hop as well. The resolved
530    /// addresses of those hosts are classified with the public-web classes too, so a listed
531    /// name that resolves to a private address is refused; opt a host out with
532    /// [`AddressPolicy::allow_private_resolution`]. Link-local and metadata destinations stay
533    /// denied and the custom denials below still apply.
534    pub fn first_party(hosts: Vec<String>) -> Self {
535        Self {
536            mode: Mode::FirstParty(hosts.iter().map(|h| normalize_host(h)).collect()),
537            ..Self::public_web()
538        }
539    }
540
541    /// True for a policy built with [`AddressPolicy::first_party`].
542    pub fn is_first_party(&self) -> bool {
543        matches!(self.mode, Mode::FirstParty(_))
544    }
545
546    /// Accept non-canonical IPv4 host spellings (`127.1`, `0177.0.0.1`, `0x7f.0.0.1`,
547    /// `2130706433`, `1.1.1.1.`) and read them as `inet_aton` does. They are refused by
548    /// default with [`crate::HttpError::AddressDenied`], because parsers disagree about
549    /// them (some read `0177.0.0.1` as decimal 177), and that mismatch is an SSRF bypass.
550    /// No legitimate URL spells a host this way. Cloud metadata and the address classes
551    /// still apply to an accepted spelling.
552    pub fn allow_legacy_ipv4(mut self) -> Self {
553        self.allow_legacy_ipv4 = true;
554        self
555    }
556
557    /// Follow redirects from HTTPS to plain HTTP. Refused by default: the redirect target
558    /// (often a signed URL) would travel unencrypted and the answer could not be trusted.
559    pub fn allow_insecure_redirects(mut self) -> Self {
560        self.allow_downgrade = true;
561        self
562    }
563
564    /// False for a redirect from HTTPS to plain HTTP, unless
565    /// [`AddressPolicy::allow_insecure_redirects`] was set. Other transports use it to apply
566    /// the same rule.
567    pub fn allows_redirect(&self, from_scheme: &str, to_scheme: &str) -> bool {
568        self.allow_downgrade
569            || !from_scheme.eq_ignore_ascii_case("https")
570            || !to_scheme.eq_ignore_ascii_case("http")
571    }
572
573    /// Refuse an HTTPS to HTTP redirect unless [`AddressPolicy::allow_insecure_redirects`].
574    pub(crate) fn check_redirect_scheme(
575        &self,
576        from_scheme: &str,
577        to_scheme: &str,
578        host: &str,
579    ) -> Result<(), AddressDenial> {
580        if self.allows_redirect(from_scheme, to_scheme) {
581            return Ok(());
582        }
583        Err(denial(
584            &normalize_host(host),
585            None,
586            None,
587            "redirect from HTTPS to HTTP refused",
588        ))
589    }
590
591    /// Let a first-party `host` resolve to non-public addresses (loopback, RFC 1918, ...),
592    /// for example an on-premises server. Exact host match like [`AddressPolicy::first_party`].
593    /// Link-local and metadata addresses, `deny_ranges` and `deny_if` still refuse. It has no
594    /// effect on hosts that are not first-party.
595    pub fn allow_private_resolution(mut self, host: impl Into<String>) -> Self {
596        self.private_ok.push(normalize_host(&host.into()));
597        self
598    }
599
600    /// Deny these ranges in addition to the class rules. An IPv6 address that embeds an
601    /// IPv4 address is also tested by its embedded IPv4 address.
602    pub fn deny_ranges(mut self, ranges: Vec<IpNet>) -> Self {
603        self.deny_ranges.extend(ranges);
604        self
605    }
606
607    /// Deny addresses for which `f` returns `Some(text)`. The text becomes the `reason` of
608    /// [`crate::HttpError::AddressDenied`]. `f` also sees the embedded IPv4 address of an
609    /// IPv6 form. Several hooks run in the order added.
610    pub fn deny_if<F>(mut self, f: F) -> Self
611    where
612        F: Fn(&IpAddr) -> Option<String> + Send + Sync + 'static,
613    {
614        self.deny_if.push(Arc::new(f));
615        self
616    }
617
618    /// Let the explicitly listed `hosts` (exact match, like [`AddressPolicy::first_party`])
619    /// reach addresses for which `pred` is true, even when their class is denied by default.
620    /// It does not apply to other hosts, never overrides link-local or metadata denials, and
621    /// never overrides `deny_ranges` or `deny_if`.
622    pub fn allow_if<F>(mut self, hosts: Vec<String>, pred: F) -> Self
623    where
624        F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
625    {
626        self.allow.push(AllowRule {
627            hosts: hosts.iter().map(|h| normalize_host(h)).collect(),
628            pred: Arc::new(pred),
629        });
630        self
631    }
632
633    /// Replace the system resolver (offline tests, pinned DNS, split-horizon setups). The
634    /// hook gets the normalized host name and returns every address for it.
635    pub fn with_resolver<F>(mut self, f: F) -> Self
636    where
637        F: Fn(&str) -> Vec<IpAddr> + Send + Sync + 'static,
638    {
639        self.resolver = Some(Arc::new(f));
640        self
641    }
642
643    /// Resolve `host` once and vet every address with exactly the rules the HTTP clients
644    /// use: the address classes, `deny_ranges`, `deny_if`, `allow_if`, the first-party host
645    /// pin and its private-resolution opt-outs, metadata names, and the rule that a name with
646    /// no address is denied. Returns the vetted socket addresses on `port`. The error is the
647    /// same [`HttpError::AddressDenied`] the clients return; a failed lookup is a
648    /// [`HttpError::Transport`] or [`HttpError::Timeout`] in [`Phase::Connect`].
649    ///
650    /// Other transports (a WebSocket, a raw TCP or gRPC client) use it to apply the identical
651    /// policy, and **must connect to the returned addresses and not look the name up again**.
652    /// A transport that cannot pin the connection to these addresses leaves a DNS-rebinding
653    /// window between this check and its own lookup.
654    ///
655    /// The host is treated as reached over HTTPS: a first-party policy checks the host pin
656    /// but cannot see the caller's transport, so requiring TLS stays the caller's job. A
657    /// system lookup is bounded at ten seconds.
658    pub fn resolve_vetted(&self, host: &str, port: u16) -> Result<Vec<SocketAddr>, HttpError> {
659        self.admit("https", host)?;
660        let host = normalize_host(host);
661        let addrs = match self.fixed_addrs(&host) {
662            Some(addrs) => addrs,
663            None => match self.lookup_blocking(&host, port, Some(Duration::from_secs(10))) {
664                Ok(addrs) => addrs,
665                Err(e) if e.kind() == io::ErrorKind::TimedOut => {
666                    return Err(HttpError::timeout("name lookup", Phase::Connect))
667                }
668                Err(_) => return Err(HttpError::transport("host not found", Phase::Connect)),
669            },
670        };
671        let vetted = self.vet(&host, addrs)?;
672        Ok(vetted
673            .into_iter()
674            .map(|ip| SocketAddr::new(ip, port))
675            .collect())
676    }
677
678    /// True when some `allow_if` rule lists `host`.
679    fn host_listed(&self, host: &str) -> bool {
680        self.allow
681            .iter()
682            .any(|rule| rule.hosts.iter().any(|h| h == host))
683    }
684
685    /// Vet a destination that a proxy will resolve, so its address cannot be checked or
686    /// pinned here. IP literals (and `localhost`) are classified like any address. Names
687    /// are refused when they are `localhost`, `*.localhost`, `*.local`, `*.internal`, or
688    /// have a single label, unless an `allow_if` rule lists the host. First-party hosts are
689    /// pinned by [`AddressPolicy::admit`] instead and skip the name rules.
690    pub(crate) fn vet_via_proxy(&self, host: &str) -> Result<(), AddressDenial> {
691        let host = normalize_host(host);
692        if let Some(addrs) = self.fixed_addrs(&host) {
693            return self.vet(&host, addrs).map(|_| ());
694        }
695        if matches!(self.mode, Mode::FirstParty(_)) || self.host_listed(&host) {
696            return Ok(());
697        }
698        let reason = if host.ends_with(".local") {
699            Some("mDNS name is refused through a proxy")
700        } else if host.ends_with(".internal") {
701            Some("internal name is refused through a proxy")
702        } else if !host.contains('.') {
703            Some("single-label name is refused through a proxy")
704        } else {
705            None
706        };
707        match reason {
708            Some(r) => Err(denial(
709                &host,
710                None,
711                Some(AddressClass::Private),
712                format!("{r} (through a proxy only names and literals can be checked)"),
713            )),
714            None => Ok(()),
715        }
716    }
717
718    /// Name- and scheme-level admission, before any lookup: metadata host names, and the
719    /// first-party host and HTTPS pin.
720    ///
721    /// `host` should be the host exactly as the URL spelled it (trailing dot included), so a
722    /// non-canonical IPv4 spelling can be refused.
723    pub(crate) fn admit(&self, scheme: &str, host: &str) -> Result<(), AddressDenial> {
724        if !self.allow_legacy_ipv4 {
725            if let Some(parsed) = parse_host_ip(host) {
726                if !parsed.is_canonical {
727                    return Err(denial(
728                        &normalize_host(host),
729                        Some(parsed.addr),
730                        Some(classify_ip(parsed.addr)),
731                        format!(
732                            "non-canonical IPv4 spelling is refused (it reads as {})",
733                            parsed.addr
734                        ),
735                    ));
736                }
737            }
738        }
739        let host = normalize_host(host);
740        if is_metadata_host(&host) {
741            return Err(denial(
742                &host,
743                None,
744                Some(AddressClass::LinkLocal),
745                "host is a cloud metadata endpoint",
746            ));
747        }
748        if let Mode::FirstParty(hosts) = &self.mode {
749            if !scheme.eq_ignore_ascii_case("https") {
750                return Err(denial(
751                    &host,
752                    None,
753                    None,
754                    "first-party client allows HTTPS only",
755                ));
756            }
757            if !hosts.contains(&host) {
758                return Err(denial(
759                    &host,
760                    None,
761                    None,
762                    "host is not in the first-party host list",
763                ));
764            }
765        }
766        Ok(())
767    }
768
769    /// Addresses that need no lookup: IP literals and `localhost` names.
770    pub(crate) fn fixed_addrs(&self, host: &str) -> Option<Vec<IpAddr>> {
771        let host = normalize_host(host);
772        if let Some(parsed) = parse_host_ip(&host) {
773            return Some(vec![parsed.addr]);
774        }
775        if host == "localhost" || host.ends_with(".localhost") {
776            return Some(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
777        }
778        None
779    }
780
781    /// Resolve `host` with the configured hook or the system resolver. `limit` bounds the
782    /// lookup, hook included. `Ok(empty)` means the name resolved to nothing.
783    pub(crate) fn lookup_blocking(
784        &self,
785        host: &str,
786        port: u16,
787        limit: Option<Duration>,
788    ) -> io::Result<Vec<IpAddr>> {
789        let host = normalize_host(host);
790        let hook = self.resolver.clone();
791        let run = move || -> io::Result<Vec<IpAddr>> {
792            match hook {
793                Some(hook) => Ok(hook(&host)),
794                None => Ok((host, port).to_socket_addrs()?.map(|a| a.ip()).collect()),
795            }
796        };
797        match limit {
798            None => run(),
799            Some(limit) => {
800                let (tx, rx) = mpsc::sync_channel(1);
801                std::thread::spawn(move || tx.send(run()).ok());
802                match rx.recv_timeout(limit) {
803                    Ok(r) => r,
804                    Err(_) => Err(io::Error::new(
805                        io::ErrorKind::TimedOut,
806                        "name lookup timed out",
807                    )),
808                }
809            }
810        }
811    }
812
813    /// The configured resolver hook, for the async resolver.
814    #[cfg(feature = "async")]
815    pub(crate) fn resolver_hook(&self) -> Option<ResolverHook> {
816        self.resolver.clone()
817    }
818
819    /// Classify every resolved address and return them when all pass. One denied address
820    /// denies the host. Names with no address are denied.
821    pub(crate) fn vet(&self, host: &str, addrs: Vec<IpAddr>) -> Result<Vec<IpAddr>, AddressDenial> {
822        self.vet_with(host, addrs)
823    }
824
825    fn vet_with(&self, host: &str, addrs: Vec<IpAddr>) -> Result<Vec<IpAddr>, AddressDenial> {
826        let host = normalize_host(host);
827        if addrs.is_empty() {
828            return Err(denial(
829                &host,
830                None,
831                None,
832                "host did not resolve to any address",
833            ));
834        }
835        let literal = parse_host_ip(&host).is_some();
836        for &ip in &addrs {
837            let class = classify_ip(ip);
838            if let Some(why) = hard_block(ip) {
839                return Err(denial(
840                    &host,
841                    Some(ip),
842                    Some(class),
843                    format!("{} {why}", subject(literal, ip)),
844                ));
845            }
846            let forms: Vec<IpAddr> = std::iter::once(ip)
847                .chain(embedded_v4s(ip).into_iter().map(IpAddr::V4))
848                .collect();
849            for form in forms.iter() {
850                if let Some(net) = self.deny_ranges.iter().find(|n| n.contains(form)) {
851                    return Err(denial(
852                        &host,
853                        Some(ip),
854                        Some(class),
855                        format!("{} is in denied range {net}", subject(literal, ip)),
856                    ));
857                }
858                for hook in &self.deny_if {
859                    if let Some(text) = hook(form) {
860                        return Err(denial(&host, Some(ip), Some(class), text));
861                    }
862                }
863            }
864            let private_ok =
865                matches!(self.mode, Mode::FirstParty(_)) && self.private_ok.contains(&host);
866            if class != AddressClass::Public && !private_ok && !self.allowed(&host, &ip) {
867                let reason = if literal {
868                    format!("address {ip} is {class:?}")
869                } else {
870                    format!("host resolves to {class:?} address {ip}")
871                };
872                return Err(denial(&host, Some(ip), Some(class), reason));
873            }
874        }
875        Ok(addrs)
876    }
877
878    fn allowed(&self, host: &str, ip: &IpAddr) -> bool {
879        self.allow
880            .iter()
881            .any(|rule| rule.hosts.iter().any(|h| h == host) && (rule.pred)(ip))
882    }
883}
884
885fn subject(literal: bool, ip: IpAddr) -> String {
886    if literal {
887        format!("address {ip}")
888    } else {
889        format!("resolved address {ip}")
890    }
891}
892
893fn denial(
894    host: &str,
895    addr: Option<IpAddr>,
896    class: Option<AddressClass>,
897    reason: impl Into<String>,
898) -> AddressDenial {
899    AddressDenial {
900        host: host.to_string(),
901        addr,
902        class,
903        reason: reason.into(),
904    }
905}
906
907/// Find an [`AddressDenial`] anywhere in an error's source chain.
908pub(crate) fn denial_in_chain(err: &(dyn Error + 'static)) -> Option<AddressDenial> {
909    let mut current: Option<&(dyn Error + 'static)> = Some(err);
910    while let Some(e) = current {
911        if let Some(d) = e.downcast_ref::<AddressDenial>() {
912            return Some(d.clone());
913        }
914        current = e.source();
915    }
916    None
917}
918
919thread_local! {
920    /// True while the current thread is running a request that goes through the proxy.
921    static VIA_PROXY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
922}
923
924/// Marks the current thread's ureq call as routed through the proxy, so the resolver
925/// treats the proxy endpoint (host and port) as trusted configuration. Without the mark
926/// the same host and port is an ordinary destination and gets the full check, which is the
927/// case for `NO_PROXY` hosts.
928pub(crate) struct ViaProxy(bool);
929
930impl ViaProxy {
931    pub(crate) fn enter(on: bool) -> Self {
932        Self(VIA_PROXY.with(|c| c.replace(on)))
933    }
934}
935
936impl Drop for ViaProxy {
937    fn drop(&mut self) {
938        VIA_PROXY.with(|c| c.set(self.0));
939    }
940}
941
942/// `ureq` keeps at most this many addresses per lookup (its `ResolvedSocketAddrs` capacity).
943const MAX_RESOLVED_ADDRS: usize = 16;
944
945/// Resolver for `ureq`: admits the URI, resolves it once, vets every address, and returns
946/// only the vetted socket addresses. `ureq`'s `TcpConnector` connects to exactly the
947/// addresses this returns (`ConnectionDetails::addrs`), so the check and the connection
948/// use the same lookup.
949#[derive(Debug)]
950pub(crate) struct VettingResolver {
951    pub(crate) policy: Arc<AddressPolicy>,
952    /// Proxy endpoint `(normalized host, port)`: resolved but trusted (configuration, not
953    /// input), so no address-class denial and no first-party host pin, but only while the
954    /// call is marked [`ViaProxy`].
955    pub(crate) proxy: Option<(String, u16)>,
956    /// Bound for a name lookup when the request has no resolve or global timeout.
957    pub(crate) lookup_limit: Duration,
958}
959
960impl ureq::unversioned::resolver::Resolver for VettingResolver {
961    fn resolve(
962        &self,
963        uri: &ureq::http::Uri,
964        _config: &ureq::config::Config,
965        timeout: ureq::unversioned::transport::NextTimeout,
966    ) -> Result<ureq::unversioned::resolver::ResolvedSocketAddrs, ureq::Error> {
967        let scheme = uri.scheme_str().unwrap_or("").to_ascii_lowercase();
968        let default_port = match scheme.as_str() {
969            "http" => 80,
970            "https" => 443,
971            _ => return Err(ureq::Error::BadUri("unsupported scheme".into())),
972        };
973        let raw_host = uri
974            .host()
975            .ok_or_else(|| ureq::Error::BadUri("missing host".into()))?;
976        let host = normalize_host(raw_host);
977        let port = uri.port_u16().unwrap_or(default_port);
978        let is_proxy = VIA_PROXY.with(|c| c.get())
979            && self
980                .proxy
981                .as_ref()
982                .is_some_and(|(h, p)| *h == host && *p == port);
983        if !is_proxy && !matches!(scheme.as_str(), "http" | "https") {
984            return Err(ureq::Error::BadUri("unsupported scheme".into()));
985        }
986        if !is_proxy {
987            self.policy.admit(&scheme, raw_host).map_err(to_ureq)?;
988        }
989        let addrs = match self.policy.fixed_addrs(&host) {
990            Some(addrs) => addrs,
991            None => {
992                let limit = if timeout.after.is_not_happening() {
993                    self.lookup_limit
994                } else {
995                    (*timeout.after).min(self.lookup_limit)
996                };
997                match self.policy.lookup_blocking(&host, port, Some(limit)) {
998                    Ok(addrs) => addrs,
999                    Err(e) if e.kind() == io::ErrorKind::TimedOut => {
1000                        return Err(ureq::Error::Timeout(timeout.reason))
1001                    }
1002                    Err(_) => return Err(ureq::Error::HostNotFound),
1003                }
1004            }
1005        };
1006        let vetted = if is_proxy {
1007            if addrs.is_empty() {
1008                return Err(ureq::Error::HostNotFound);
1009            }
1010            addrs
1011        } else {
1012            self.policy.vet(&host, addrs).map_err(to_ureq)?
1013        };
1014        let mut out = ureq::unversioned::resolver::Resolver::empty(self);
1015        for ip in vetted.into_iter().take(MAX_RESOLVED_ADDRS) {
1016            out.push(std::net::SocketAddr::new(ip, port));
1017        }
1018        Ok(out)
1019    }
1020}
1021
1022fn to_ureq(d: AddressDenial) -> ureq::Error {
1023    ureq::Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, d))
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028    use super::*;
1029
1030    fn ip(s: &str) -> IpAddr {
1031        s.parse().unwrap()
1032    }
1033
1034    #[test]
1035    fn ipnet_parses_and_matches() {
1036        let net: IpNet = "10.0.0.0/8".parse().unwrap();
1037        assert!(net.contains(&ip("10.255.0.1")));
1038        assert!(!net.contains(&ip("11.0.0.1")));
1039        assert!(!net.contains(&ip("::1")));
1040        let v6: IpNet = "fd00::/8".parse().unwrap();
1041        assert!(v6.contains(&ip("fd12::1")));
1042        assert!(!v6.contains(&ip("fe80::1")));
1043        let host: IpNet = "203.0.113.9".parse().unwrap();
1044        assert_eq!(host.prefix(), 32);
1045        assert!(host.contains(&ip("203.0.113.9")) && !host.contains(&ip("203.0.113.8")));
1046        assert!("10.0.0.0/33".parse::<IpNet>().is_err());
1047        assert!("nonsense".parse::<IpNet>().is_err());
1048        let all: IpNet = "0.0.0.0/0".parse().unwrap();
1049        assert!(all.contains(&ip("8.8.8.8")));
1050    }
1051
1052    #[test]
1053    fn embedded_forms_extract_the_v4() {
1054        assert_eq!(
1055            embedded_v4(ip("::ffff:10.1.2.3")),
1056            Some("10.1.2.3".parse().unwrap())
1057        );
1058        assert_eq!(
1059            embedded_v4(ip("::10.1.2.3")),
1060            Some("10.1.2.3".parse().unwrap())
1061        );
1062        assert_eq!(
1063            embedded_v4(ip("64:ff9b::a00:1")),
1064            Some("10.0.0.1".parse().unwrap())
1065        );
1066        assert_eq!(
1067            embedded_v4(ip("2002:0a00:0001::1")),
1068            Some("10.0.0.1".parse().unwrap())
1069        );
1070        assert_eq!(embedded_v4(ip("::1")), None);
1071        assert_eq!(embedded_v4(ip("::")), None);
1072        assert_eq!(embedded_v4(ip("2606:4700::1")), None);
1073        assert_eq!(embedded_v4(ip("1.2.3.4")), None);
1074    }
1075
1076    #[test]
1077    fn host_ip_parsing_reports_the_spelling() {
1078        let k = |h: &str| parse_host_ip(h).map(|p| (p.addr, p.kind, p.is_canonical));
1079        let v4 = |s: &str| IpAddr::V4(s.parse().unwrap());
1080        assert_eq!(
1081            k("1.1.1.1"),
1082            Some((v4("1.1.1.1"), HostIpKind::Ipv4DottedQuad, true))
1083        );
1084        for (spelling, want) in [
1085            ("127.1", "127.0.0.1"),
1086            ("10.1.2", "10.1.0.2"),
1087            ("0177.0.0.1", "127.0.0.1"),
1088            ("0x7f.0.0.1", "127.0.0.1"),
1089            ("0X7F000001", "127.0.0.1"),
1090            ("2130706433", "127.0.0.1"),
1091            ("017700000001", "127.0.0.1"),
1092            ("0251.0376.0251.0376", "169.254.169.254"),
1093            ("1.1.1.1.", "1.1.1.1"),
1094            ("0", "0.0.0.0"),
1095        ] {
1096            assert_eq!(
1097                k(spelling),
1098                Some((v4(want), HostIpKind::Ipv4Legacy, false)),
1099                "{spelling}"
1100            );
1101        }
1102        for v6 in [
1103            "::1",
1104            "[::1]",
1105            "::ffff:127.0.0.1",
1106            "[::ffff:7f00:1]",
1107            "64:ff9b::a00:1",
1108        ] {
1109            let p = parse_host_ip(v6).unwrap();
1110            assert!(p.is_canonical && p.kind == HostIpKind::Ipv6, "{v6}");
1111        }
1112        for not_ip in [
1113            "example.com",
1114            "1.2.3.4.5",
1115            "256.1.1.1",
1116            "1.2.3.256",
1117            "4294967296",
1118            "08.1",
1119            "0x",
1120            "1..2",
1121            "",
1122            ".",
1123            "1.2.3.",
1124            "a.b.c.d",
1125            "127.0.0.1x",
1126        ] {
1127            let got = parse_host_ip(not_ip);
1128            // "1.2.3." has a trailing dot on a valid 3-part spelling: that is a legacy IPv4.
1129            if not_ip == "1.2.3." {
1130                assert!(got.is_some());
1131            } else {
1132                assert!(got.is_none(), "{not_ip}: {got:?}");
1133            }
1134        }
1135        assert_eq!(
1136            parse_legacy_ipv4("0x7f.1"),
1137            Some("127.0.0.1".parse().unwrap())
1138        );
1139        assert_eq!(parse_legacy_ipv4("1.1.1.1."), None);
1140        assert_eq!(parse_legacy_ipv4("::1"), None);
1141    }
1142
1143    #[test]
1144    fn non_canonical_spellings_are_refused_by_default_and_allowed_on_request() {
1145        let strict = AddressPolicy::default();
1146        for h in ["0177.0.0.1", "0x7f.1", "2130706433", "127.1", "1.1.1.1."] {
1147            let d = strict.admit("http", h).unwrap_err();
1148            assert!(d.reason.contains("non-canonical"), "{h}: {d}");
1149            assert!(d.addr.is_some(), "{h}");
1150        }
1151        for h in [
1152            "1.1.1.1",
1153            "::ffff:1.1.1.1",
1154            "[::1]",
1155            "example.com",
1156            "example.com.",
1157        ] {
1158            assert!(strict.admit("http", h).is_ok(), "{h}");
1159        }
1160        let lax = AddressPolicy::default().allow_legacy_ipv4();
1161        assert!(lax.admit("http", "127.1").is_ok());
1162        // The address classes still apply to an accepted spelling.
1163        assert!(lax.vet("127.1", lax.fixed_addrs("127.1").unwrap()).is_err());
1164    }
1165
1166    #[test]
1167    fn host_normalization() {
1168        assert_eq!(normalize_host("[::1]"), "::1");
1169        assert_eq!(normalize_host("Example.COM."), "example.com");
1170    }
1171
1172    #[test]
1173    fn vet_denies_empty_resolution() {
1174        let d = AddressPolicy::default()
1175            .vet("nowhere.test", vec![])
1176            .unwrap_err();
1177        assert_eq!(d.addr, None);
1178        assert!(d.reason.contains("did not resolve"), "{d}");
1179    }
1180}