Skip to main content

aauth_protocol/client/
guard.rs

1//! What an agent may dial on someone else's say-so. A Person Server URL is
2//! often chosen by a tenant, and the agent calls it from its own network
3//! position, so it must never reach that network itself (SSRF).
4//!
5//! Two halves, because reqwest dials an IP-literal URL without resolving it:
6//! [`admit_host`] refuses literals and `localhost` before a request is built,
7//! and [`PublicResolver`] is the client's resolver, so the addresses it checked
8//! are the addresses the socket connects to.
9
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
11use std::sync::Arc;
12use std::time::Duration;
13
14use ipnet::IpNet;
15use reqwest::dns::{Addrs, Name, Resolve, Resolving};
16use reqwest::Url;
17
18use crate::error::{Error, Result};
19
20/// Ranges no tenant URL may resolve into, beyond the ones [`is_public`] knows:
21/// a plane whose pod or service CIDR is not RFC 1918 lists it here.
22#[derive(Clone, Debug, Default)]
23pub struct DeniedNetworks(Vec<IpNet>);
24
25impl DeniedNetworks {
26    /// `cidr,cidr,...`, e.g. `240.10.0.0/16,2600:1900:4000::/44`.
27    pub fn parse(raw: &str) -> Result<Self> {
28        raw.split(',')
29            .map(str::trim)
30            .filter(|entry| !entry.is_empty())
31            .map(|entry| {
32                entry
33                    .parse::<IpNet>()
34                    .map_err(|e| Error::Invalid(format!("denied network {entry:?}: {e}")))
35            })
36            .collect::<Result<_, _>>()
37            .map(Self)
38    }
39
40    fn allows(&self, ip: IpAddr) -> bool {
41        is_public(ip) && !self.0.iter().any(|net| net.contains(&ip))
42    }
43}
44
45fn is_public_v4(ip: Ipv4Addr) -> bool {
46    let [a, b, c, _] = ip.octets();
47    !(ip.is_unspecified()
48        || ip.is_loopback()
49        || ip.is_private()
50        || ip.is_link_local()
51        || ip.is_broadcast()
52        || ip.is_multicast()
53        || ip.is_documentation()
54        || a == 0
55        // CGNAT, 100.64.0.0/10.
56        || (a == 100 && (b & 0xc0) == 64)
57        // IETF protocol assignments, 192.0.0.0/24.
58        || (a == 192 && b == 0 && c == 0)
59        // Benchmarking, 198.18.0.0/15.
60        || (a == 198 && (b & 0xfe) == 18)
61        // Reserved, 240.0.0.0/4.
62        || a >= 240)
63}
64
65/// Whether `ip` is a public unicast address.
66pub fn is_public(ip: IpAddr) -> bool {
67    match ip {
68        IpAddr::V4(v4) => is_public_v4(v4),
69        IpAddr::V6(v6) => {
70            if let Some(v4) = embedded_v4(v6) {
71                return is_public_v4(v4);
72            }
73            let first = v6.segments()[0];
74            // 6to4, 2002::/16, carries an IPv4 address in its next 32 bits.
75            if first == 0x2002 {
76                let s = v6.segments();
77                return is_public_v4(Ipv4Addr::new(
78                    (s[1] >> 8) as u8,
79                    s[1] as u8,
80                    (s[2] >> 8) as u8,
81                    s[2] as u8,
82                ));
83            }
84            !(v6.is_unspecified()
85                || v6.is_loopback()
86                || v6.is_multicast()
87                // Unique local, fc00::/7.
88                || (first & 0xfe00) == 0xfc00
89                // Link-local, fe80::/10.
90                || (first & 0xffc0) == 0xfe80
91                // Documentation, 2001:db8::/32.
92                || (first == 0x2001 && v6.segments()[1] == 0x0db8)
93                // Local-use NAT64, 64:ff9b:1::/48.
94                || (first == 0x64 && v6.segments()[1] == 0xff9b && v6.segments()[2] == 1)
95                // Discard-only, 100::/64.
96                || (first == 0x0100 && v6.segments()[1..4] == [0, 0, 0]))
97        }
98    }
99}
100
101/// The IPv4 address an IPv4-mapped (`::ffff:a.b.c.d`) or NAT64
102/// (`64:ff9b::a.b.c.d`) address reaches.
103fn embedded_v4(v6: Ipv6Addr) -> Option<Ipv4Addr> {
104    if let Some(v4) = v6.to_ipv4_mapped() {
105        return Some(v4);
106    }
107    let s = v6.segments();
108    (s[..6] == [0x64, 0xff9b, 0, 0, 0, 0])
109        .then(|| Ipv4Addr::new((s[6] >> 8) as u8, s[6] as u8, (s[7] >> 8) as u8, s[7] as u8))
110}
111
112/// Refuse a URL whose host is an IP literal or `localhost`: the resolver never
113/// sees those.
114pub fn admit_host(url: &str) -> Result<()> {
115    let parsed = Url::parse(url).map_err(|e| Error::Url(format!("{url}: {e}")))?;
116    let host = parsed
117        .host_str()
118        .ok_or_else(|| Error::Url(format!("refusing to call {url}: no host")))?;
119    if host.trim_matches(['[', ']']).parse::<IpAddr>().is_ok() {
120        return Err(Error::Url(format!("refusing to call {url}: an IP address")));
121    }
122    let host = host.trim_end_matches('.').to_ascii_lowercase();
123    if host == "localhost" || host.ends_with(".localhost") {
124        return Err(Error::Url(format!("refusing to call {url}: localhost")));
125    }
126    Ok(())
127}
128
129/// Resolves through the system resolver and fails the lookup when any answer
130/// is not public, so a name cannot resolve one way when checked and another
131/// when dialled.
132pub struct PublicResolver {
133    denied: DeniedNetworks,
134}
135
136impl PublicResolver {
137    /// A resolver refusing non-public addresses and anything in `denied`.
138    pub fn new(denied: DeniedNetworks) -> Self {
139        Self { denied }
140    }
141}
142
143impl Resolve for PublicResolver {
144    fn resolve(&self, name: Name) -> Resolving {
145        let denied = self.denied.clone();
146        let host = name.as_str().to_string();
147        Box::pin(async move {
148            let addrs: Vec<SocketAddr> =
149                tokio::net::lookup_host((host.as_str(), 0)).await?.collect();
150            if addrs.is_empty() {
151                return Err(format!("{host} resolved to nothing").into());
152            }
153            if let Some(refused) = addrs.iter().find(|addr| !denied.allows(addr.ip())) {
154                return Err(
155                    format!("{host} resolves to {}, not a public address", refused.ip()).into(),
156                );
157            }
158            Ok(Box::new(addrs.into_iter()) as Addrs)
159        })
160    }
161}
162
163/// An HTTP client for URLs someone else chose: a 10-second timeout, no
164/// redirects, no proxy (which would resolve names past this resolver), and
165/// a resolver that refuses any name answering with a non-public address or
166/// one in `denied`.
167///
168/// reqwest dials an IP-literal URL without resolving it, so pair this with
169/// [`admit_host`] on every URL, as [`crate::discovery::ReqwestFetch::guarded`]
170/// and [`super::PersonServerClient`] do.
171pub fn guarded_client(denied: DeniedNetworks) -> Result<reqwest::Client> {
172    reqwest::Client::builder()
173        .timeout(Duration::from_secs(10))
174        .redirect(reqwest::redirect::Policy::none())
175        .no_proxy()
176        .dns_resolver(Arc::new(PublicResolver::new(denied)))
177        .build()
178        .map_err(|e| Error::Http(format!("could not build the guarded client: {e}")))
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    fn public(ip: &str) -> bool {
186        is_public(ip.parse().unwrap())
187    }
188
189    #[test]
190    fn private_loopback_link_local_and_cgnat_addresses_are_not_public() {
191        for ip in [
192            "127.0.0.1",
193            "10.1.2.3",
194            "172.16.0.1",
195            "192.168.1.1",
196            "169.254.169.254",
197            "100.64.0.1",
198            "0.0.0.0",
199            "240.0.0.1",
200            "::1",
201            "fd00::1",
202            "fe80::1",
203            "::ffff:10.0.0.1",
204            "::ffff:169.254.169.254",
205            "64:ff9b::a9fe:a9fe",
206            "2002:a9fe:a9fe::1",
207            "64:ff9b:1::a9fe:a9fe",
208        ] {
209            assert!(!public(ip), "{ip} should not be public");
210        }
211        for ip in ["8.8.8.8", "100.128.0.1", "2001:4860:4860::8888"] {
212            assert!(public(ip), "{ip} should be public");
213        }
214    }
215
216    #[test]
217    fn a_listed_network_is_refused_too() {
218        let denied = DeniedNetworks::parse("34.118.224.0/20, 2600:1900::/28").unwrap();
219        assert!(!denied.allows("34.118.230.1".parse().unwrap()));
220        assert!(!denied.allows("2600:1900::1".parse().unwrap()));
221        assert!(denied.allows("34.120.0.1".parse().unwrap()));
222        assert!(DeniedNetworks::parse("10.0.0.0").is_err());
223    }
224
225    #[test]
226    fn ip_literals_and_localhost_are_refused_before_resolving() {
227        for url in [
228            "https://127.0.0.1/token",
229            "https://10.0.0.5/",
230            "https://169.254.169.254/latest/meta-data",
231            "https://[::1]:8443/",
232            "https://localhost/",
233            "https://ps.localhost/",
234        ] {
235            assert!(admit_host(url).is_err(), "{url} should be refused");
236        }
237        assert!(admit_host("https://ps.example.com/token").is_ok());
238    }
239
240    #[tokio::test]
241    async fn a_name_resolving_private_is_refused() {
242        let resolver = PublicResolver::new(DeniedNetworks::default());
243        let refused = resolver.resolve("localhost".parse().unwrap()).await;
244        assert!(refused.is_err());
245    }
246}