|
| 1 | +use std::collections::BTreeMap; |
| 2 | +use std::io; |
| 3 | +use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs}; |
| 4 | +use std::sync::Mutex; |
| 5 | +use std::time::{Duration, Instant}; |
| 6 | + |
| 7 | +const PUBLIC_RESOLVERS: [&str; 2] = ["1.1.1.1:53", "8.8.8.8:53"]; |
| 8 | +const RESPONSE_TIMEOUT: Duration = Duration::from_secs(1); |
| 9 | +const CACHE_TTL: Duration = Duration::from_secs(300); |
| 10 | +const TYPE_A: u16 = 1; |
| 11 | + |
| 12 | +fn cache() -> &'static Mutex<BTreeMap<String, CachedAddresses>> { |
| 13 | + static CACHE: Mutex<BTreeMap<String, CachedAddresses>> = Mutex::new(BTreeMap::new()); |
| 14 | + &CACHE |
| 15 | +} |
| 16 | + |
| 17 | +struct CachedAddresses { |
| 18 | + at: Instant, |
| 19 | + addresses: Vec<SocketAddr>, |
| 20 | +} |
| 21 | + |
| 22 | +pub fn resolve(netloc: &str) -> io::Result<Vec<SocketAddr>> { |
| 23 | + let (host, port) = split_netloc(netloc)?; |
| 24 | + if let Some(cached) = cached(&host) { |
| 25 | + return Ok(cached); |
| 26 | + } |
| 27 | + let usable = filter_usable(system_addresses(&host, port)); |
| 28 | + let addresses = if usable.is_empty() { |
| 29 | + query_public(&host, port) |
| 30 | + } else { |
| 31 | + usable |
| 32 | + }; |
| 33 | + if addresses.is_empty() { |
| 34 | + Err(io::Error::other(format!( |
| 35 | + "dns resolution failed for {host}" |
| 36 | + ))) |
| 37 | + } else { |
| 38 | + remember(&host, &addresses); |
| 39 | + Ok(addresses) |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +fn cached(host: &str) -> Option<Vec<SocketAddr>> { |
| 44 | + let cache = cache().lock().ok()?; |
| 45 | + let cached = cache.get(host)?; |
| 46 | + if cached.at.elapsed() < CACHE_TTL { |
| 47 | + Some(cached.addresses.clone()) |
| 48 | + } else { |
| 49 | + None |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +fn remember(host: &str, addresses: &[SocketAddr]) { |
| 54 | + if let Ok(mut cache) = cache().lock() { |
| 55 | + cache.insert( |
| 56 | + host.to_string(), |
| 57 | + CachedAddresses { |
| 58 | + at: Instant::now(), |
| 59 | + addresses: addresses.to_vec(), |
| 60 | + }, |
| 61 | + ); |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +fn split_netloc(netloc: &str) -> io::Result<(String, u16)> { |
| 66 | + let (host, port) = netloc |
| 67 | + .rsplit_once(':') |
| 68 | + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "netloc without port"))?; |
| 69 | + let port = port |
| 70 | + .parse() |
| 71 | + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid port"))?; |
| 72 | + Ok((host.to_string(), port)) |
| 73 | +} |
| 74 | + |
| 75 | +fn system_addresses(host: &str, port: u16) -> Vec<SocketAddr> { |
| 76 | + format!("{host}:{port}") |
| 77 | + .to_socket_addrs() |
| 78 | + .map(|addrs| addrs.collect()) |
| 79 | + .unwrap_or_default() |
| 80 | +} |
| 81 | + |
| 82 | +fn filter_usable(addresses: Vec<SocketAddr>) -> Vec<SocketAddr> { |
| 83 | + addresses |
| 84 | + .into_iter() |
| 85 | + .filter(|address| !address.ip().is_unspecified()) |
| 86 | + .collect() |
| 87 | +} |
| 88 | + |
| 89 | +fn query_public(host: &str, port: u16) -> Vec<SocketAddr> { |
| 90 | + PUBLIC_RESOLVERS |
| 91 | + .iter() |
| 92 | + .filter_map(|resolver| { |
| 93 | + query_a_records(host, resolver).map(|records| { |
| 94 | + records |
| 95 | + .into_iter() |
| 96 | + .map(|ip| SocketAddr::new(ip, port)) |
| 97 | + .collect::<Vec<_>>() |
| 98 | + }) |
| 99 | + }) |
| 100 | + .next() |
| 101 | + .unwrap_or_default() |
| 102 | +} |
| 103 | + |
| 104 | +fn query_a_records(host: &str, resolver: &str) -> Option<Vec<IpAddr>> { |
| 105 | + let socket = std::net::UdpSocket::bind("0.0.0.0:0").ok()?; |
| 106 | + socket.set_read_timeout(Some(RESPONSE_TIMEOUT)).ok()?; |
| 107 | + let query = build_query(host)?; |
| 108 | + socket.send_to(&query, resolver).ok()?; |
| 109 | + let mut buffer = [0u8; 1024]; |
| 110 | + let received = socket.recv(&mut buffer).ok()?; |
| 111 | + parse_a_records(&buffer[..received]).filter(|records| !records.is_empty()) |
| 112 | +} |
| 113 | + |
| 114 | +fn build_query(host: &str) -> Option<Vec<u8>> { |
| 115 | + let mut query = vec![0x53, 0x1f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; |
| 116 | + for label in host.split('.') { |
| 117 | + let length = label.len(); |
| 118 | + if length == 0 || length > 63 { |
| 119 | + return None; |
| 120 | + } |
| 121 | + query.push(length as u8); |
| 122 | + query.extend_from_slice(label.as_bytes()); |
| 123 | + } |
| 124 | + query.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0x01]); |
| 125 | + Some(query) |
| 126 | +} |
| 127 | + |
| 128 | +fn parse_a_records(packet: &[u8]) -> Option<Vec<IpAddr>> { |
| 129 | + if packet.len() < 12 { |
| 130 | + return None; |
| 131 | + } |
| 132 | + let answers = u16::from_be_bytes([packet[6], packet[7]]) as usize; |
| 133 | + let mut offset = 12; |
| 134 | + while offset < packet.len() && packet[offset] != 0 { |
| 135 | + offset += packet[offset] as usize + 1; |
| 136 | + } |
| 137 | + offset += 5; |
| 138 | + let mut records = Vec::new(); |
| 139 | + for _ in 0..answers { |
| 140 | + offset = skip_name(packet, offset)?; |
| 141 | + if offset + 10 > packet.len() { |
| 142 | + return None; |
| 143 | + } |
| 144 | + let record_type = u16::from_be_bytes([packet[offset], packet[offset + 1]]); |
| 145 | + let record_length = u16::from_be_bytes([packet[offset + 8], packet[offset + 9]]) as usize; |
| 146 | + let data_start = offset + 10; |
| 147 | + if data_start + record_length > packet.len() { |
| 148 | + return None; |
| 149 | + } |
| 150 | + if record_type == TYPE_A && record_length == 4 { |
| 151 | + records.push(IpAddr::V4(Ipv4Addr::new( |
| 152 | + packet[data_start], |
| 153 | + packet[data_start + 1], |
| 154 | + packet[data_start + 2], |
| 155 | + packet[data_start + 3], |
| 156 | + ))); |
| 157 | + } |
| 158 | + offset = data_start + record_length; |
| 159 | + } |
| 160 | + Some(records) |
| 161 | +} |
| 162 | + |
| 163 | +fn skip_name(packet: &[u8], mut offset: usize) -> Option<usize> { |
| 164 | + loop { |
| 165 | + let byte = *packet.get(offset)?; |
| 166 | + if byte & 0xC0 == 0xC0 { |
| 167 | + return Some(offset + 2); |
| 168 | + } |
| 169 | + if byte == 0 { |
| 170 | + return Some(offset + 1); |
| 171 | + } |
| 172 | + offset += byte as usize + 1; |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +#[cfg(test)] |
| 177 | +mod tests { |
| 178 | + use super::*; |
| 179 | + |
| 180 | + #[test] |
| 181 | + fn cache_round_trips_addresses() { |
| 182 | + let host = "cache.test"; |
| 183 | + assert!(cached(host).is_none()); |
| 184 | + remember( |
| 185 | + host, |
| 186 | + &[SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 443)], |
| 187 | + ); |
| 188 | + assert_eq!( |
| 189 | + cached(host).unwrap(), |
| 190 | + vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 443)] |
| 191 | + ); |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn build_query_encodes_a_record_lookup() { |
| 196 | + let query = build_query("api.country.is").unwrap(); |
| 197 | + assert_eq!(&query[0..2], &[0x53, 0x1f]); |
| 198 | + assert_eq!(&query[2..4], &[0x01, 0x00]); |
| 199 | + assert_eq!( |
| 200 | + &query[10..], |
| 201 | + &[ |
| 202 | + 3, b'a', b'p', b'i', 7, b'c', b'o', b'u', b'n', b't', b'r', b'y', 2, b'i', b's', 0, |
| 203 | + 0, 1, 0, 1 |
| 204 | + ] |
| 205 | + ); |
| 206 | + } |
| 207 | + |
| 208 | + #[test] |
| 209 | + fn build_query_rejects_empty_labels() { |
| 210 | + assert!(build_query("").is_none()); |
| 211 | + } |
| 212 | + |
| 213 | + #[test] |
| 214 | + fn parse_a_records_skips_cname_and_reads_addresses() { |
| 215 | + let mut packet = vec![ |
| 216 | + 0x53, 0x1f, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, |
| 217 | + ]; |
| 218 | + for label in "api.country.is".split('.') { |
| 219 | + packet.push(label.len() as u8); |
| 220 | + packet.extend_from_slice(label.as_bytes()); |
| 221 | + } |
| 222 | + packet.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0x01]); |
| 223 | + packet.extend_from_slice(&[ |
| 224 | + 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7B, 0x00, 0x00, |
| 225 | + ]); |
| 226 | + packet.extend_from_slice(&[ |
| 227 | + 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7B, 0x00, 0x04, 1, 2, 3, 4, |
| 228 | + ]); |
| 229 | + let records = parse_a_records(&packet).unwrap(); |
| 230 | + assert_eq!(records, vec![IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))]); |
| 231 | + } |
| 232 | + |
| 233 | + #[test] |
| 234 | + fn parse_a_records_rejects_truncated_packets() { |
| 235 | + assert!(parse_a_records(&[0x53, 0x1f]).is_none()); |
| 236 | + assert!(parse_a_records(&[]).is_none()); |
| 237 | + } |
| 238 | + |
| 239 | + #[test] |
| 240 | + fn filter_usable_drops_unspecified_addresses() { |
| 241 | + let addresses = vec![ |
| 242 | + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 443), |
| 243 | + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 443), |
| 244 | + SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), 443), |
| 245 | + ]; |
| 246 | + let usable = filter_usable(addresses); |
| 247 | + assert_eq!(usable.len(), 1); |
| 248 | + assert_eq!(usable[0].ip(), IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); |
| 249 | + } |
| 250 | + |
| 251 | + #[test] |
| 252 | + fn split_netloc_parses_host_and_port() { |
| 253 | + let (host, port) = split_netloc("api.country.is:443").unwrap(); |
| 254 | + assert_eq!(host, "api.country.is"); |
| 255 | + assert_eq!(port, 443); |
| 256 | + assert!(split_netloc("no-port").is_err()); |
| 257 | + } |
| 258 | +} |
0 commit comments