1use std::fmt;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4use ipnet::{Ipv4Net, Ipv6Net};
5
6use crate::errors::HostError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
10pub enum LocalAddress {
11 #[error("Requests to loopback addresses are not allowed")]
12 Loopback,
13 #[error("Requests to the local network are not allowed")]
14 LocalNetwork,
15}
16
17#[derive(Debug, Clone, Copy, Default)]
18pub struct AllowedLocal {
19 pub localhost: bool,
20 pub local_network: bool,
21}
22
23impl AllowedLocal {
24 const fn lets_through(self, local: LocalAddress) -> bool {
25 match local {
26 LocalAddress::Loopback => self.localhost,
27 LocalAddress::LocalNetwork => self.local_network,
28 }
29 }
30}
31
32pub enum Host<'a> {
33 Ip(IpAddr),
34 Name(&'a str),
35}
36
37impl fmt::Display for Host<'_> {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 Self::Ip(ip) => ip.fmt(f),
41 Self::Name(name) => f.write_str(name),
42 }
43 }
44}
45
46const LOOPBACK_V4: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(127, 0, 0, 0), 8);
47
48const LOCAL_NETWORK_V4: [Ipv4Net; 14] = [
50 Ipv4Net::new_assert(Ipv4Addr::UNSPECIFIED, 8),
51 Ipv4Net::new_assert(Ipv4Addr::new(10, 0, 0, 0), 8),
52 Ipv4Net::new_assert(Ipv4Addr::new(100, 64, 0, 0), 10),
53 Ipv4Net::new_assert(Ipv4Addr::new(169, 254, 0, 0), 16),
54 Ipv4Net::new_assert(Ipv4Addr::new(172, 16, 0, 0), 12),
55 Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 0), 24),
56 Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 170), 31),
57 Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 2, 0), 24),
58 Ipv4Net::new_assert(Ipv4Addr::new(192, 168, 0, 0), 16),
59 Ipv4Net::new_assert(Ipv4Addr::new(198, 18, 0, 0), 15),
60 Ipv4Net::new_assert(Ipv4Addr::new(198, 51, 100, 0), 24),
61 Ipv4Net::new_assert(Ipv4Addr::new(203, 0, 113, 0), 24),
62 Ipv4Net::new_assert(Ipv4Addr::new(240, 0, 0, 0), 4),
63 Ipv4Net::new_assert(Ipv4Addr::BROADCAST, 32),
64];
65
66const GLOBAL_V4: [Ipv4Addr; 2] = [Ipv4Addr::new(192, 0, 0, 9), Ipv4Addr::new(192, 0, 0, 10)];
67
68const LOCAL_NETWORK_V6: [Ipv6Net; 9] = [
69 Ipv6Net::new_assert(Ipv6Addr::UNSPECIFIED, 128),
70 Ipv6Net::new_assert(Ipv6Addr::new(0x64, 0xff9b, 1, 0, 0, 0, 0, 0), 48),
71 Ipv6Net::new_assert(Ipv6Addr::new(0x100, 0, 0, 0, 0, 0, 0, 0), 64),
72 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0), 23),
73 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32),
74 Ipv6Net::new_assert(Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0), 16),
75 Ipv6Net::new_assert(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0), 20),
76 Ipv6Net::new_assert(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7),
77 Ipv6Net::new_assert(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), 10),
78];
79
80const GLOBAL_V6: [Ipv6Net; 6] = [
81 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 1), 128),
82 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 2), 128),
83 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 3, 0, 0, 0, 0, 0, 0), 32),
84 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 4, 0x112, 0, 0, 0, 0, 0), 48),
85 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0x20, 0, 0, 0, 0, 0, 0), 28),
86 Ipv6Net::new_assert(Ipv6Addr::new(0x2001, 0x30, 0, 0, 0, 0, 0, 0), 28),
87];
88
89#[must_use]
91pub fn local_address(ip: IpAddr) -> Option<LocalAddress> {
92 match ip {
93 IpAddr::V4(ip) => local_v4(ip),
94 IpAddr::V6(ip) => ip.to_ipv4_mapped().map_or_else(|| local_v6(ip), local_v4),
95 }
96}
97
98pub async fn outside_addresses(
99 host: &Host<'_>,
100 allowed: AllowedLocal,
101) -> Result<Vec<IpAddr>, HostError> {
102 let ips = match host {
103 Host::Ip(ip) => vec![*ip],
104 Host::Name(name) => tokio::net::lookup_host((*name, 0))
105 .await
106 .map_err(HostError::Unresolved)?
107 .map(|address| address.ip())
108 .collect(),
109 };
110 if ips.is_empty() {
111 return Err(HostError::NoAddresses);
112 }
113 let refused = ips
114 .iter()
115 .filter_map(|ip| local_address(*ip))
116 .find(|local| !allowed.lets_through(*local));
117 refused.map_or(Ok(ips), |local| Err(HostError::Local(local)))
118}
119
120fn local_v4(ip: Ipv4Addr) -> Option<LocalAddress> {
121 if LOOPBACK_V4.contains(&ip) {
122 Some(LocalAddress::Loopback)
123 } else if LOCAL_NETWORK_V4.iter().any(|net| net.contains(&ip)) && !GLOBAL_V4.contains(&ip) {
124 Some(LocalAddress::LocalNetwork)
125 } else {
126 None
127 }
128}
129
130fn local_v6(ip: Ipv6Addr) -> Option<LocalAddress> {
131 if ip.is_loopback() {
132 Some(LocalAddress::Loopback)
133 } else if LOCAL_NETWORK_V6.iter().any(|net| net.contains(&ip))
134 && !GLOBAL_V6.iter().any(|net| net.contains(&ip))
135 {
136 Some(LocalAddress::LocalNetwork)
137 } else {
138 None
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use std::net::IpAddr;
145
146 use super::{local_address, LocalAddress};
147
148 fn kind(ip: &str) -> Option<LocalAddress> {
149 local_address(ip.parse::<IpAddr>().unwrap())
150 }
151
152 #[test]
155 fn a_loopback_address_is_loopback() {
156 for ip in ["127.0.0.1", "127.255.0.1", "::1", "::ffff:127.0.0.1"] {
157 assert_eq!(kind(ip), Some(LocalAddress::Loopback), "{ip}");
158 }
159 }
160
161 #[test]
162 fn a_special_purpose_address_is_local_network() {
163 for ip in [
164 "0.0.0.0",
165 "10.0.0.1",
166 "100.64.0.1",
167 "169.254.1.1",
168 "172.16.0.1",
169 "192.0.0.1",
170 "192.0.0.171",
171 "192.0.2.1",
172 "192.168.0.1",
173 "198.19.0.1",
174 "198.51.100.1",
175 "203.0.113.1",
176 "240.0.0.1",
177 "255.255.255.255",
178 "::",
179 "::ffff:10.0.0.1",
180 "64:ff9b:1::1",
181 "100::1",
182 "2001:10::1",
183 "2001:db8::1",
184 "2002::1",
185 "3fff::1",
186 "fc00::1",
187 "fe80::1",
188 ] {
189 assert_eq!(kind(ip), Some(LocalAddress::LocalNetwork), "{ip}");
190 }
191 }
192
193 #[test]
195 fn a_mapped_ipv4_is_read_as_its_ipv4() {
196 assert_eq!(kind("::ffff:100.64.0.1"), Some(LocalAddress::LocalNetwork));
197 }
198
199 #[test]
200 fn a_global_address_is_not_local() {
201 for ip in [
202 "8.8.8.8",
203 "192.0.0.9",
204 "192.0.0.10",
205 "224.0.0.1",
206 "::ffff:8.8.8.8",
207 "::127.0.0.1",
208 "64:ff9b::a00:1",
209 "2001:1::1",
210 "2001:3::1",
211 "2001:4:112::1",
212 "2001:20::1",
213 "2001:30::1",
214 "2606:4700::1111",
215 ] {
216 assert_eq!(kind(ip), None, "{ip}");
217 }
218 }
219}