aauth_protocol/client/
guard.rs1use 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#[derive(Clone, Debug, Default)]
23pub struct DeniedNetworks(Vec<IpNet>);
24
25impl DeniedNetworks {
26 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 || (a == 100 && (b & 0xc0) == 64)
57 || (a == 192 && b == 0 && c == 0)
59 || (a == 198 && (b & 0xfe) == 18)
61 || a >= 240)
63}
64
65pub 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 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 || (first & 0xfe00) == 0xfc00
89 || (first & 0xffc0) == 0xfe80
91 || (first == 0x2001 && v6.segments()[1] == 0x0db8)
93 || (first == 0x64 && v6.segments()[1] == 0xff9b && v6.segments()[2] == 1)
95 || (first == 0x0100 && v6.segments()[1..4] == [0, 0, 0]))
97 }
98 }
99}
100
101fn 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
112pub 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
129pub struct PublicResolver {
133 denied: DeniedNetworks,
134}
135
136impl PublicResolver {
137 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
163pub 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}