This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1use std::collections::BTreeSet; 2use std::convert::Infallible; 3use std::net::{IpAddr, Ipv6Addr, SocketAddr}; 4use std::str::FromStr; 5 6use ipnet::IpNet; 7 8pub const MAX_HOPS: usize = 32; 9 10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 11struct ProxyNet(IpNet); 12 13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] 14pub enum ProxyNetError { 15 #[error("`{0}` isn't an IP address or a CIDR block")] 16 Unreadable(String), 17 #[error("a blank entry isn't an IP address or a CIDR block, so drop it or fill it in")] 18 Blank, 19} 20 21impl ProxyNet { 22 fn contains(self, peer: IpAddr) -> bool { 23 match peer.to_canonical() { 24 IpAddr::V4(v4) => { 25 self.0.contains(&IpAddr::V4(v4)) 26 || self.0.contains(&IpAddr::V6(v4.to_ipv6_mapped())) 27 } 28 v6 => self.0.contains(&v6), 29 } 30 } 31} 32 33impl FromStr for ProxyNet { 34 type Err = ProxyNetError; 35 36 fn from_str(raw: &str) -> Result<Self, Self::Err> { 37 match raw.trim() { 38 "" => Err(ProxyNetError::Blank), 39 entry => entry 40 .parse::<IpNet>() 41 .ok() 42 .or_else(|| entry.parse::<IpAddr>().ok().map(IpNet::from)) 43 .map(|net| Self(canonical(net))) 44 .ok_or_else(|| ProxyNetError::Unreadable(entry.to_owned())), 45 } 46 } 47} 48 49fn canonical(net: IpNet) -> IpNet { 50 match net { 51 IpNet::V6(v6) => match (v6.network().to_canonical(), v6.prefix_len()) { 52 (v4 @ IpAddr::V4(_), len @ 96..) => IpNet::new_assert(v4, len - 96), 53 _ => net, 54 }, 55 IpNet::V4(_) => net, 56 } 57} 58 59fn chain_address(entry: &str) -> Option<IpAddr> { 60 entry 61 .parse::<IpAddr>() 62 .ok() 63 .or_else(|| entry.parse::<SocketAddr>().ok().map(|hop| hop.ip())) 64 .or_else(|| { 65 entry 66 .strip_prefix('[') 67 .and_then(|rest| rest.strip_suffix(']')) 68 .and_then(|inner| inner.parse::<Ipv6Addr>().ok()) 69 .map(IpAddr::V6) 70 }) 71} 72 73pub fn comma_separated(raw: &str) -> Result<Vec<String>, Infallible> { 74 Ok(raw 75 .split(',') 76 .map(str::trim) 77 .filter(|entry| !entry.is_empty()) 78 .map(str::to_owned) 79 .collect()) 80} 81 82#[derive(Debug, Clone, Default, PartialEq, Eq)] 83pub struct TrustedProxies(BTreeSet<ProxyNet>); 84 85impl TrustedProxies { 86 pub fn parse<'a>(entries: impl IntoIterator<Item = &'a str>) -> Result<Self, ProxyNetError> { 87 entries 88 .into_iter() 89 .map(ProxyNet::from_str) 90 .collect::<Result<BTreeSet<_>, _>>() 91 .map(Self) 92 } 93 94 pub fn is_empty(&self) -> bool { 95 self.0.is_empty() 96 } 97 98 pub fn contains(&self, peer: IpAddr) -> bool { 99 self.0.iter().any(|net| net.contains(peer)) 100 } 101 102 pub fn rightmost_untrusted<'a, I>(&self, chain: I) -> Option<IpAddr> 103 where 104 I: IntoIterator<Item = &'a str>, 105 I::IntoIter: DoubleEndedIterator, 106 { 107 chain 108 .into_iter() 109 .rev() 110 .map(str::trim) 111 .filter(|entry| !entry.is_empty()) 112 .take(MAX_HOPS) 113 .find_map(|entry| match chain_address(entry) { 114 Some(address) if self.contains(address) => None, 115 parsed => Some(parsed), 116 }) 117 .flatten() 118 .map(|address| address.to_canonical()) 119 } 120} 121 122#[cfg(test)] 123mod tests { 124 use super::*; 125 126 fn ip(value: &str) -> IpAddr { 127 value.parse().unwrap() 128 } 129 130 fn trusting<'a>(entries: impl IntoIterator<Item = &'a str>) -> TrustedProxies { 131 TrustedProxies::parse(entries).unwrap() 132 } 133 134 fn client(proxies: &TrustedProxies, chain: &str) -> Option<IpAddr> { 135 proxies.rightmost_untrusted(chain.split(',')) 136 } 137 138 #[test] 139 fn rightmost_untrusted_takes_the_rightmost_entry_with_an_empty_list() { 140 let anyone = TrustedProxies::default(); 141 [ 142 ("203.0.113.7, 198.51.100.4", Some("198.51.100.4")), 143 (" 192.0.2.1 ", Some("192.0.2.1")), 144 ("not-an-ip", None), 145 ("", None), 146 ] 147 .iter() 148 .for_each(|&(chain, expected)| { 149 assert_eq!(client(&anyone, chain), expected.map(ip), "{chain:?}"); 150 }); 151 } 152 153 #[test] 154 fn rightmost_untrusted_steps_over_every_listed_hop_and_stops_where_it_cant_parse() { 155 let proxies = trusting(["198.51.100.4"]); 156 [ 157 ( 158 "203.0.113.7, 198.51.100.4", 159 Some("203.0.113.7"), 160 "the hop left of the listed proxy is the client", 161 ), 162 ( 163 "203.0.113.7, 198.51.100.4, 198.51.100.4", 164 Some("203.0.113.7"), 165 "two listed hops in a row are both stepped over", 166 ), 167 ( 168 "203.0.113.7, 192.0.2.1, 198.51.100.4", 169 Some("192.0.2.1"), 170 "rightmost_untrusted stops at the first unlisted hop, even with a client still further left", 171 ), 172 ( 173 "198.51.100.4", 174 None, 175 "rightmost_untrusted won't find a client in a chain of listed hops alone", 176 ), 177 ( 178 "203.0.113.7, not-an-ip, 198.51.100.4", 179 None, 180 "rightmost_untrusted stops at an entry it can't parse, because a client can write anything left of the proxy", 181 ), 182 ( 183 "203.0.113.7, 198.51.100.4,", 184 Some("203.0.113.7"), 185 "a stray comma mustn't cost the header, since a client can't claim anything through a blank", 186 ), 187 ( 188 ",203.0.113.7,, 198.51.100.4", 189 Some("203.0.113.7"), 190 "a blank anywhere else in the chain reads the same way", 191 ), 192 ( 193 "203.0.113.7:4321, 198.51.100.4", 194 Some("203.0.113.7"), 195 "some proxies write the hop with the port it connected from", 196 ), 197 ( 198 "[2001:db8::1]:443, 198.51.100.4", 199 Some("2001:db8::1"), 200 "and bracket an IPv6 hop when they do", 201 ), 202 ( 203 "[2001:db8::1], 198.51.100.4", 204 Some("2001:db8::1"), 205 "brackets turn up without a port too", 206 ), 207 ( 208 "2001:db8::1, 198.51.100.4", 209 Some("2001:db8::1"), 210 "a bare IPv6 hop doesn't need unwrapping", 211 ), 212 ] 213 .iter() 214 .for_each(|&(chain, expected, why)| { 215 assert_eq!(client(&proxies, chain), expected.map(ip), "{why}: {chain:?}"); 216 }); 217 assert_eq!( 218 client( 219 &trusting(["198.51.100.0/24"]), 220 "203.0.113.7, 198.51.100.9, 198.51.100.10" 221 ), 222 Some(ip("203.0.113.7")), 223 "one CIDR entry covers every hop inside the block" 224 ); 225 } 226 227 #[test] 228 fn rightmost_untrusted_reads_only_the_last_max_hops_entries() { 229 let proxies = trusting(["198.51.100.4"]); 230 let padded = |hops| { 231 std::iter::once("203.0.113.7") 232 .chain(std::iter::repeat_n("198.51.100.4", hops)) 233 .collect::<Vec<_>>() 234 .join(",") 235 }; 236 assert_eq!( 237 client(&proxies, &padded(MAX_HOPS - 1)), 238 Some(ip("203.0.113.7")), 239 "a chain within MAX_HOPS still reaches the client behind every listed hop" 240 ); 241 assert_eq!( 242 client(&proxies, &padded(MAX_HOPS)), 243 None, 244 "the peer answers for the address it connected from when the last MAX_HOPS entries are all listed hops" 245 ); 246 } 247 248 #[test] 249 fn an_entry_covers_every_address_it_spans_in_either_spelling() { 250 [ 251 (&["198.51.100.0/24"][..], "198.51.100.4", true, "a CIDR entry covers the addresses inside it"), 252 (&["198.51.100.0/24"], "198.51.100.255", true, "up to the last address in the block"), 253 (&["198.51.100.0/24"], "198.51.101.1", false, "and stops at the block boundary"), 254 (&["2001:db8::/32"], "2001:db8::dead:beef", true, "an IPv6 block reads the same way"), 255 (&["2001:db8::/32"], "2001:db9::1", false, "and stops at its boundary too"), 256 (&["2001:db8::/32"], "198.51.100.4", false, "an IPv6 block that isn't v4-mapped won't cover an IPv4 address"), 257 (&["::ffff:198.51.100.0/120"], "198.51.100.4", true, "a v4-mapped block covers the plain v4 addresses inside it"), 258 (&["::ffff:198.51.100.0/120"], "::ffff:198.51.100.4", true, "in either spelling"), 259 (&["::ffff:198.51.100.0/120"], "198.51.101.4", false, "and stops at the folded block boundary"), 260 (&["::ffff:0:0/96"], "203.0.113.7", true, "the whole v4-mapped range folds to every IPv4 address"), 261 (&["::ffff:0:0/95"], "203.0.113.7", true, "the match has to try the mapped spelling too, because a prefix under 96 keeps the block in IPv6, where the plain v4 spelling of a peer would miss it"), 262 (&["::/0"], "203.0.113.7", true, "an operator who lists every IPv6 address has listed every v4-mapped address with it"), 263 (&["127.0.0.1", " ::1 "], "127.0.0.1", true, "a bare address is a single host, and its entry may be padded"), 264 (&["127.0.0.1", " ::1 "], "::ffff:127.0.0.1", true, "which a dual-stack listener may report mapped"), 265 (&["127.0.0.1", " ::1 "], "::1", true, "the IPv6 loopback is its own entry"), 266 (&["127.0.0.1", " ::1 "], "127.0.0.2", false, "and the host next door is outside all of them"), 267 (&[], "203.0.113.7", false, "a caller that reads an empty list as trusting every peer has to say so itself, since rightmost_untrusted would step over every entry and never find a client"), 268 ] 269 .iter() 270 .for_each(|&(entries, peer, expected, why)| { 271 assert_eq!( 272 trusting(entries.iter().copied()).contains(ip(peer)), 273 expected, 274 "{why}: {entries:?} against {peer}" 275 ); 276 }); 277 assert!(TrustedProxies::default().is_empty()); 278 } 279 280 #[test] 281 fn parse_refuses_an_entry_it_cant_read_and_quotes_it() { 282 assert_eq!( 283 TrustedProxies::parse(["127.0.0.1:5555"]) 284 .unwrap_err() 285 .to_string(), 286 "`127.0.0.1:5555` isn't an IP address or a CIDR block", 287 ); 288 [vec![""], vec![" "], vec!["127.0.0.1", "\t"]] 289 .into_iter() 290 .for_each(|entries| { 291 assert_eq!( 292 TrustedProxies::parse(entries.iter().copied()), 293 Err(ProxyNetError::Blank), 294 "discarding the blank would leave a list the operator filled in reading as empty. A caller is free to read an empty list as trusting every peer: {entries:?}" 295 ); 296 }); 297 } 298 299 #[test] 300 fn comma_separated_discards_the_gaps_a_separator_leaves_behind() { 301 assert_eq!( 302 comma_separated("127.0.0.1, 173.245.48.0/20,").unwrap(), 303 vec!["127.0.0.1".to_owned(), "173.245.48.0/20".to_owned()], 304 "comma_separated mustn't leave a blank for parse to refuse, since a trailing separator belongs to the format" 305 ); 306 assert!(comma_separated("").unwrap().is_empty()); 307 } 308}