Monorepo for Tangled
tangled.org
1use std::error::Error as StdError;
2use std::fmt;
3use std::io;
4use std::net::SocketAddr;
5
6use reqwest::dns::{Addrs, Name, Resolve, Resolving};
7
8use crate::host::{PrivateHostReason, classify_ip};
9
10pub struct PrivateAddressFilter {
11 allow_private: bool,
12}
13
14impl PrivateAddressFilter {
15 pub fn new(allow_private: bool) -> Self {
16 Self { allow_private }
17 }
18}
19
20impl Resolve for PrivateAddressFilter {
21 fn resolve(&self, name: Name) -> Resolving {
22 let allow_private = self.allow_private;
23 let host = name.as_str().to_owned();
24 Box::pin(async move {
25 let resolved: Vec<SocketAddr> =
26 tokio::net::lookup_host((host.as_str(), 0)).await?.collect();
27 partition_safe(host, allow_private, resolved)
28 })
29 }
30}
31
32fn partition_safe(
33 host: String,
34 allow_private: bool,
35 resolved: Vec<SocketAddr>,
36) -> Result<Addrs, Box<dyn StdError + Send + Sync>> {
37 if !allow_private && let Some(reason) = resolved.iter().find_map(|sa| classify_ip(&sa.ip())) {
38 return Err(Box::new(BlockedAddressError { host, reason }));
39 }
40 if resolved.is_empty() {
41 return Err(Box::new(io::Error::other(format!(
42 "no resolvable addresses for {host}"
43 ))));
44 }
45 Ok(Box::new(resolved.into_iter()))
46}
47
48#[derive(Debug)]
49pub(crate) struct BlockedAddressError {
50 host: String,
51 reason: PrivateHostReason,
52}
53
54impl fmt::Display for BlockedAddressError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(
57 f,
58 "dns resolution for {} returned {} address",
59 self.host, self.reason,
60 )
61 }
62}
63
64impl StdError for BlockedAddressError {}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use std::net::{IpAddr, Ipv4Addr};
70
71 fn sa(ip: &str, port: u16) -> SocketAddr {
72 SocketAddr::new(ip.parse().unwrap(), port)
73 }
74
75 #[test]
76 fn blocks_when_any_resolved_address_is_private_under_strict() {
77 let mixed = vec![sa("8.8.8.8", 0), sa("127.0.0.1", 0)];
78 let res = partition_safe("mixed.example".into(), false, mixed);
79 assert!(res.is_err(), "any private address must fail strict resolve");
80 }
81
82 #[test]
83 fn allows_all_when_permissive() {
84 let mixed = vec![sa("8.8.8.8", 0), sa("127.0.0.1", 0)];
85 let res = partition_safe("mixed.example".into(), true, mixed).expect("permissive");
86 let collected: Vec<SocketAddr> = res.collect();
87 assert_eq!(collected.len(), 2);
88 }
89
90 #[test]
91 fn permits_public_only_resolution_under_strict() {
92 let public = vec![sa("8.8.8.8", 0), sa("1.1.1.1", 0)];
93 let res = partition_safe("public.example".into(), false, public).expect("public");
94 let collected: Vec<SocketAddr> = res.collect();
95 assert_eq!(collected.len(), 2);
96 }
97
98 #[test]
99 fn empty_resolution_is_an_error() {
100 let res = partition_safe("nx.example".into(), false, vec![]);
101 assert!(res.is_err(), "empty address list must surface an error");
102 }
103
104 #[test]
105 fn classify_ip_matches_url_classifier() {
106 assert!(classify_ip(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))).is_some());
107 assert!(classify_ip(&IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))).is_none());
108 }
109}