Skip to main content

fluidattacks_core_git/
local_address.rs

1use std::fmt;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3
4use ipnet::{Ipv4Net, Ipv6Net};
5use reqwest::dns::{Addrs, Name, Resolve, Resolving};
6use url::Url;
7
8use crate::errors::{HostError, LocalAddress};
9
10const LOOPBACK_V4: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(127, 0, 0, 0), 8);
11
12// IANA's special-purpose ranges, as Python's ipaddress reads them; std's is_global, which covers them, is nightly-only.
13const LOCAL_NETWORK_V4: [Ipv4Net; 14] = [
14    Ipv4Net::new_assert(Ipv4Addr::UNSPECIFIED, 8),
15    Ipv4Net::new_assert(Ipv4Addr::new(10, 0, 0, 0), 8),
16    Ipv4Net::new_assert(Ipv4Addr::new(100, 64, 0, 0), 10),
17    Ipv4Net::new_assert(Ipv4Addr::new(169, 254, 0, 0), 16),
18    Ipv4Net::new_assert(Ipv4Addr::new(172, 16, 0, 0), 12),
19    Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 0), 24),
20    Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 170), 31),
21    Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 2, 0), 24),
22    Ipv4Net::new_assert(Ipv4Addr::new(192, 168, 0, 0), 16),
23    Ipv4Net::new_assert(Ipv4Addr::new(198, 18, 0, 0), 15),
24    Ipv4Net::new_assert(Ipv4Addr::new(198, 51, 100, 0), 24),
25    Ipv4Net::new_assert(Ipv4Addr::new(203, 0, 113, 0), 24),
26    Ipv4Net::new_assert(Ipv4Addr::new(240, 0, 0, 0), 4),
27    Ipv4Net::new_assert(Ipv4Addr::BROADCAST, 32),
28];
29
30const GLOBAL_V4: [Ipv4Addr; 2] = [Ipv4Addr::new(192, 0, 0, 9), Ipv4Addr::new(192, 0, 0, 10)];
31
32const LOCAL_NETWORK_V6: [Ipv6Net; 9] = [
33    Ipv6Net::new_assert(Ipv6Addr::UNSPECIFIED, 128),
34    Ipv6Net::new_assert(Ipv6Addr::new(0x64, 0xff9b, 1, 0, 0, 0, 0, 0), 48),
35    Ipv6Net::new_assert(Ipv6Addr::new(0x100, 0, 0, 0, 0, 0, 0, 0), 64),
36    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0), 23),
37    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32),
38    Ipv6Net::new_assert(Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0), 16),
39    Ipv6Net::new_assert(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0), 20),
40    Ipv6Net::new_assert(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7),
41    Ipv6Net::new_assert(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), 10),
42];
43
44const GLOBAL_V6: [Ipv6Net; 6] = [
45    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 1), 128),
46    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 2), 128),
47    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 3, 0, 0, 0, 0, 0, 0), 32),
48    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 4, 0x112, 0, 0, 0, 0, 0), 48),
49    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0x20, 0, 0, 0, 0, 0, 0), 28),
50    Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0x30, 0, 0, 0, 0, 0, 0), 28),
51];
52
53#[derive(Debug, Clone, Copy, Default)]
54pub struct AllowedLocal {
55    pub localhost: bool,
56    pub local_network: bool,
57}
58
59impl AllowedLocal {
60    const fn lets_through(self, local: LocalAddress) -> bool {
61        match local {
62            LocalAddress::Loopback => self.localhost,
63            LocalAddress::LocalNetwork => self.local_network,
64        }
65    }
66}
67
68pub enum Host<'a> {
69    Ip(IpAddr),
70    Name(&'a str),
71}
72
73impl fmt::Display for Host<'_> {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Ip(ip) => ip.fmt(f),
77            Self::Name(name) => f.write_str(name),
78        }
79    }
80}
81
82/// Hands reqwest only the addresses it checked, so a second lookup cannot swap in a local one.
83pub(crate) struct ProtectedResolver(pub(crate) AllowedLocal);
84
85impl Resolve for ProtectedResolver {
86    fn resolve(&self, name: Name) -> Resolving {
87        let allowed = self.0;
88        Box::pin(async move {
89            let ips = outside_addresses(&Host::Name(name.as_str()), allowed).await?;
90            let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
91            Ok(addrs)
92        })
93    }
94}
95
96/// `None` for an address reachable from outside; an IPv4 mapped into IPv6 counts as its IPv4.
97#[must_use]
98pub fn local_address(ip: IpAddr) -> Option<LocalAddress> {
99    match ip {
100        IpAddr::V4(ip) => local_v4(ip),
101        IpAddr::V6(ip) => ip.to_ipv4_mapped().map_or_else(|| local_v6(ip), local_v4),
102    }
103}
104
105pub async fn outside_addresses(
106    host: &Host<'_>,
107    allowed: AllowedLocal,
108) -> Result<Vec<IpAddr>, HostError> {
109    let ips = match host {
110        Host::Ip(ip) => vec![*ip],
111        Host::Name(name) => tokio::net::lookup_host((*name, 0))
112            .await
113            .map_err(HostError::Unresolved)?
114            .map(|address| address.ip())
115            .collect(),
116    };
117    if ips.is_empty() {
118        return Err(HostError::NoAddresses);
119    }
120    let refused = ips
121        .iter()
122        .filter_map(|ip| local_address(*ip))
123        .find(|local| !allowed.lets_through(*local));
124    refused.map_or(Ok(ips), |local| Err(HostError::Local(local)))
125}
126
127/// Refuses the url when it is written with a local IP; a name is left to `ProtectedResolver`.
128pub(crate) fn refuse_local_ip(url: &Url, allowed: AllowedLocal) -> Result<(), LocalAddress> {
129    let ip = match url.host() {
130        Some(url::Host::Ipv4(ip)) => IpAddr::V4(ip),
131        Some(url::Host::Ipv6(ip)) => IpAddr::V6(ip),
132        _ => return Ok(()),
133    };
134    local_address(ip)
135        .filter(|local| !allowed.lets_through(*local))
136        .map_or(Ok(()), Err)
137}
138
139fn local_v4(ip: Ipv4Addr) -> Option<LocalAddress> {
140    if LOOPBACK_V4.contains(&ip) {
141        Some(LocalAddress::Loopback)
142    } else if LOCAL_NETWORK_V4.iter().any(|net| net.contains(&ip)) && !GLOBAL_V4.contains(&ip) {
143        Some(LocalAddress::LocalNetwork)
144    } else {
145        None
146    }
147}
148
149fn local_v6(ip: Ipv6Addr) -> Option<LocalAddress> {
150    if ip.is_loopback() {
151        Some(LocalAddress::Loopback)
152    } else if LOCAL_NETWORK_V6.iter().any(|net| net.contains(&ip))
153        && !GLOBAL_V6.iter().any(|net| net.contains(&ip))
154    {
155        Some(LocalAddress::LocalNetwork)
156    } else {
157        None
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use std::net::IpAddr;
164
165    use reqwest::dns::Resolve;
166
167    use super::{local_address, AllowedLocal, ProtectedResolver};
168    use crate::errors::{HostError, LocalAddress};
169
170    fn kind(ip: &str) -> Option<LocalAddress> {
171        local_address(ip.parse::<IpAddr>().unwrap())
172    }
173
174    // Each address read by Python 3.13's ipaddress: is_loopback, or is_private,
175    // is_link_local, 100.64.0.0/10 or 255.255.255.255.
176    #[test]
177    fn a_loopback_address_is_loopback() {
178        for ip in ["127.0.0.1", "127.255.0.1", "::1", "::ffff:127.0.0.1"] {
179            assert_eq!(kind(ip), Some(LocalAddress::Loopback), "{ip}");
180        }
181    }
182
183    #[test]
184    fn a_special_purpose_address_is_local_network() {
185        for ip in [
186            "0.0.0.0",
187            "10.0.0.1",
188            "100.64.0.1",
189            "169.254.1.1",
190            "172.16.0.1",
191            "192.0.0.1",
192            "192.0.0.171",
193            "192.0.2.1",
194            "192.168.0.1",
195            "198.19.0.1",
196            "198.51.100.1",
197            "203.0.113.1",
198            "240.0.0.1",
199            "255.255.255.255",
200            "::",
201            "::ffff:10.0.0.1",
202            "64:ff9b:1::1",
203            "100::1",
204            "2001:10::1",
205            "2001:db8::1",
206            "2002::1",
207            "3fff::1",
208            "fc00::1",
209            "fe80::1",
210        ] {
211            assert_eq!(kind(ip), Some(LocalAddress::LocalNetwork), "{ip}");
212        }
213    }
214
215    // Python reads 100.64.0.0/10 on the plain IPv4 only, and lets the mapped one through.
216    #[test]
217    fn a_mapped_ipv4_is_read_as_its_ipv4() {
218        assert_eq!(kind("::ffff:100.64.0.1"), Some(LocalAddress::LocalNetwork));
219    }
220
221    #[test]
222    fn a_global_address_is_not_local() {
223        for ip in [
224            "8.8.8.8",
225            "192.0.0.9",
226            "192.0.0.10",
227            "224.0.0.1",
228            "::ffff:8.8.8.8",
229            "::127.0.0.1",
230            "64:ff9b::a00:1",
231            "2001:1::1",
232            "2001:3::1",
233            "2001:4:112::1",
234            "2001:20::1",
235            "2001:30::1",
236            "2606:4700::1111",
237        ] {
238            assert_eq!(kind(ip), None, "{ip}");
239        }
240    }
241
242    #[tokio::test]
243    async fn the_resolver_refuses_a_name_that_resolves_to_a_local_address() {
244        let resolved = ProtectedResolver(AllowedLocal::default())
245            .resolve("localhost".parse().unwrap())
246            .await;
247
248        let error = resolved.err().unwrap();
249        assert!(
250            matches!(
251                error.downcast_ref::<HostError>(),
252                Some(HostError::Local(LocalAddress::Loopback))
253            ),
254            "{error:?}"
255        );
256    }
257}