1use std::net::{IpAddr, Ipv6Addr, SocketAddr};
33use std::sync::{Arc, OnceLock};
34
35use reqwest::dns::{Addrs, Name, Resolve, Resolving};
36
37static DEFAULT_EGRESS_POLICY: OnceLock<EgressPolicy> = OnceLock::new();
42
43pub fn set_default_egress_policy(policy: EgressPolicy) -> Result<(), EgressPolicy> {
48 DEFAULT_EGRESS_POLICY.set(policy)
49}
50
51pub fn default_egress_policy() -> EgressPolicy {
54 DEFAULT_EGRESS_POLICY
55 .get()
56 .copied()
57 .unwrap_or_else(EgressPolicy::default)
58}
59
60#[derive(Debug, Clone, Copy)]
66pub struct EgressPolicy {
67 block_link_local: bool,
68 block_cloud_metadata: bool,
69 block_loopback: bool,
70 block_private: bool,
71}
72
73impl Default for EgressPolicy {
74 fn default() -> Self {
75 Self {
76 block_link_local: true,
77 block_cloud_metadata: true,
78 block_loopback: false,
79 block_private: false,
80 }
81 }
82}
83
84impl EgressPolicy {
85 pub fn permissive() -> Self {
88 Self {
89 block_link_local: false,
90 block_cloud_metadata: false,
91 block_loopback: false,
92 block_private: false,
93 }
94 }
95
96 pub fn strict() -> Self {
100 Self {
101 block_link_local: true,
102 block_cloud_metadata: true,
103 block_loopback: true,
104 block_private: true,
105 }
106 }
107
108 pub fn with_block_link_local(mut self, block: bool) -> Self {
109 self.block_link_local = block;
110 self
111 }
112 pub fn with_block_cloud_metadata(mut self, block: bool) -> Self {
113 self.block_cloud_metadata = block;
114 self
115 }
116 pub fn with_block_loopback(mut self, block: bool) -> Self {
117 self.block_loopback = block;
118 self
119 }
120 pub fn with_block_private(mut self, block: bool) -> Self {
121 self.block_private = block;
122 self
123 }
124
125 pub fn permit_ip(&self, ip: IpAddr) -> Result<(), EgressDenial> {
127 match ip {
128 IpAddr::V4(v4) => {
129 if self.block_link_local && v4.is_link_local() {
130 return Err(EgressDenial::LinkLocal(ip));
131 }
132 if self.block_loopback && v4.is_loopback() {
133 return Err(EgressDenial::Loopback(ip));
134 }
135 if self.block_private && v4.is_private() {
136 return Err(EgressDenial::Private(ip));
137 }
138 if self.block_link_local
143 && (v4.is_broadcast() || v4.is_multicast() || v4.is_unspecified())
144 {
145 return Err(EgressDenial::LinkLocal(ip));
146 }
147 }
148 IpAddr::V6(v6) => {
149 let segs = v6.segments();
150 if self.block_link_local && (segs[0] & 0xffc0) == 0xfe80 {
151 return Err(EgressDenial::LinkLocal(ip));
152 }
153 if self.block_cloud_metadata && is_known_cloud_metadata_v6(v6) {
154 return Err(EgressDenial::CloudMetadata(ip));
155 }
156 if self.block_loopback && v6.is_loopback() {
157 return Err(EgressDenial::Loopback(ip));
158 }
159 if self.block_private && (segs[0] & 0xfe00) == 0xfc00 {
162 return Err(EgressDenial::Private(ip));
163 }
164 if self.block_link_local && (v6.is_multicast() || v6.is_unspecified()) {
165 return Err(EgressDenial::LinkLocal(ip));
166 }
167 if let Some(v4) = v6.to_ipv4_mapped() {
171 return self.permit_ip(IpAddr::V4(v4));
172 }
173 }
174 }
175 Ok(())
176 }
177}
178
179fn is_known_cloud_metadata_v6(v6: Ipv6Addr) -> bool {
180 v6 == Ipv6Addr::new(0xfd00, 0x00ec, 0x0002, 0, 0, 0, 0, 0x0254)
184}
185
186#[derive(Debug, Clone)]
188pub enum EgressDenial {
189 LinkLocal(IpAddr),
190 CloudMetadata(IpAddr),
191 Loopback(IpAddr),
192 Private(IpAddr),
193}
194
195impl std::fmt::Display for EgressDenial {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 Self::LinkLocal(ip) => {
199 write!(f, "egress policy denied link-local address {ip}")
200 }
201 Self::CloudMetadata(ip) => {
202 write!(f, "egress policy denied cloud-metadata address {ip}")
203 }
204 Self::Loopback(ip) => write!(f, "egress policy denied loopback address {ip}"),
205 Self::Private(ip) => write!(f, "egress policy denied private address {ip}"),
206 }
207 }
208}
209
210impl std::error::Error for EgressDenial {}
211
212pub struct EgressFilteredResolver {
217 policy: EgressPolicy,
218}
219
220impl EgressFilteredResolver {
221 pub fn new(policy: EgressPolicy) -> Self {
222 Self { policy }
223 }
224
225 pub fn into_dns_resolver(self) -> Arc<Self> {
230 Arc::new(self)
231 }
232}
233
234impl Resolve for EgressFilteredResolver {
235 fn resolve(&self, name: Name) -> Resolving {
236 let policy = self.policy;
237 let host = name.as_str().to_string();
238 Box::pin(async move {
239 let lookup_target = format!("{host}:0");
242 let resolved: Vec<SocketAddr> = tokio::net::lookup_host(lookup_target)
243 .await
244 .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?
245 .collect();
246
247 let mut allowed: Vec<SocketAddr> = Vec::with_capacity(resolved.len());
248 let mut first_denial: Option<EgressDenial> = None;
249 for sa in resolved {
250 match policy.permit_ip(sa.ip()) {
251 Ok(()) => allowed.push(sa),
252 Err(denial) => {
253 if first_denial.is_none() {
254 first_denial = Some(denial);
255 }
256 }
257 }
258 }
259 if allowed.is_empty() {
260 let message: String = match first_denial {
261 Some(d) => d.to_string(),
262 None => format!("no addresses resolved for '{host}'"),
263 };
264 return Err(Box::<dyn std::error::Error + Send + Sync>::from(message));
265 }
266 let addrs: Addrs = Box::new(allowed.into_iter());
267 Ok(addrs)
268 })
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use std::net::{Ipv4Addr, Ipv6Addr};
276
277 fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
278 IpAddr::V4(Ipv4Addr::new(a, b, c, d))
279 }
280
281 #[allow(clippy::too_many_arguments)]
282 fn v6(s0: u16, s1: u16, s2: u16, s3: u16, s4: u16, s5: u16, s6: u16, s7: u16) -> IpAddr {
283 IpAddr::V6(Ipv6Addr::new(s0, s1, s2, s3, s4, s5, s6, s7))
284 }
285
286 #[test]
287 fn default_blocks_link_local_and_cloud_metadata() {
288 let p = EgressPolicy::default();
289 assert!(matches!(
290 p.permit_ip(v4(169, 254, 169, 254)),
291 Err(EgressDenial::LinkLocal(_))
292 ));
293 assert!(matches!(
294 p.permit_ip(v6(0xfe80, 0, 0, 0, 0, 0, 0, 1)),
295 Err(EgressDenial::LinkLocal(_))
296 ));
297 assert!(matches!(
298 p.permit_ip(v6(0xfd00, 0x00ec, 0x0002, 0, 0, 0, 0, 0x0254)),
299 Err(EgressDenial::CloudMetadata(_))
300 ));
301 }
302
303 #[test]
304 fn default_allows_loopback_and_private() {
305 let p = EgressPolicy::default();
308 assert!(p.permit_ip(v4(127, 0, 0, 1)).is_ok());
309 assert!(p.permit_ip(v4(10, 0, 0, 1)).is_ok());
310 assert!(p.permit_ip(v4(192, 168, 1, 1)).is_ok());
311 assert!(p.permit_ip(v4(8, 8, 8, 8)).is_ok());
312 assert!(p.permit_ip(v6(0, 0, 0, 0, 0, 0, 0, 1)).is_ok());
313 }
314
315 #[test]
316 fn strict_blocks_loopback_and_private() {
317 let p = EgressPolicy::strict();
318 assert!(matches!(
319 p.permit_ip(v4(127, 0, 0, 1)),
320 Err(EgressDenial::Loopback(_))
321 ));
322 assert!(matches!(
323 p.permit_ip(v4(10, 0, 0, 1)),
324 Err(EgressDenial::Private(_))
325 ));
326 assert!(matches!(
327 p.permit_ip(v6(0xfc00, 0, 0, 0, 0, 0, 0, 1)),
328 Err(EgressDenial::Private(_))
329 ));
330 assert!(p.permit_ip(v4(8, 8, 8, 8)).is_ok());
332 }
333
334 #[test]
335 fn permissive_allows_everything() {
336 let p = EgressPolicy::permissive();
337 assert!(p.permit_ip(v4(169, 254, 169, 254)).is_ok());
338 assert!(p.permit_ip(v4(127, 0, 0, 1)).is_ok());
339 assert!(p.permit_ip(v4(10, 0, 0, 1)).is_ok());
340 assert!(
341 p.permit_ip(v6(0xfd00, 0x00ec, 0x0002, 0, 0, 0, 0, 0x0254))
342 .is_ok()
343 );
344 }
345
346 #[test]
347 fn ipv4_mapped_ipv6_inherits_v4_rules() {
348 let p = EgressPolicy::default();
351 let mapped = Ipv4Addr::new(169, 254, 169, 254).to_ipv6_mapped();
352 assert!(matches!(
353 p.permit_ip(IpAddr::V6(mapped)),
354 Err(EgressDenial::LinkLocal(_))
355 ));
356 }
357
358 #[test]
359 fn builder_overrides_individual_categories() {
360 let p = EgressPolicy::default().with_block_link_local(false);
365 assert!(p.permit_ip(v4(169, 254, 169, 254)).is_ok());
366 assert!(matches!(
367 p.permit_ip(v6(0xfd00, 0x00ec, 0x0002, 0, 0, 0, 0, 0x0254)),
368 Err(EgressDenial::CloudMetadata(_))
369 ));
370 }
371
372 #[tokio::test(flavor = "multi_thread")]
373 async fn filtered_resolver_denies_link_local_lookup() {
374 let resolver = EgressFilteredResolver::new(EgressPolicy::default());
377 let name: reqwest::dns::Name = "169.254.169.254".parse().unwrap();
378 let result = resolver.resolve(name).await;
379 let err = match result {
382 Ok(_) => panic!("policy must deny link-local literal"),
383 Err(e) => e,
384 };
385 let msg = format!("{err}");
386 assert!(
387 msg.contains("link-local"),
388 "expected link-local denial, got: {msg}"
389 );
390 }
391
392 #[tokio::test(flavor = "multi_thread")]
393 async fn filtered_resolver_permits_public_lookup() {
394 let resolver = EgressFilteredResolver::new(EgressPolicy::default());
397 let name: reqwest::dns::Name = "8.8.8.8".parse().unwrap();
398 if resolver.resolve(name).await.is_err() {
399 panic!("public IP must be permitted by default policy");
400 }
401 }
402}