oxedyne/fe2o3/fe2o3_net/src/dns_resolver.rs
11.3 KiB, 46 runs
created by r1870400018:9840, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Minimal DNS over UDP client. |
| 2 | //! |
| 3 | //! Implements the small slice of RFC 1035 needed by the SMTP outbound |
| 4 | //! client: A and MX lookups against the first nameserver listed in |
| 5 | //! `/etc/resolv.conf`, with no caching, no recursion fallback, no |
| 6 | //! truncation handling, no DNSSEC. |
| 7 | //! |
| 8 | //! The motivation is to keep Hematite free of a third-party resolver |
| 9 | //! crate. Outbound SMTP needs MX lookups and `std::net` does not expose |
| 10 | //! them; rather than add `hickory-resolver` we own the ~250 lines. |
| 11 | //! |
| 12 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 13 | //! Anthropic Claude |
| 14 | |
| 15 | use oxedyne_fe2o3_core::prelude::*; |
| 16 | |
| 17 | use std::{ |
| 18 | net::{ |
| 19 | IpAddr, |
| 20 | Ipv4Addr, |
| 21 | SocketAddr, |
| 22 | UdpSocket, |
| 23 | }, |
| 24 | time::Duration, |
| 25 | }; |
| 26 | |
| 27 | |
| 28 | /// One MX record returned by [`lookup_mx`]. |
| 29 | #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] |
| 30 | pub struct MxRecord { |
| 31 | pub preference: u16, // lower is preferred |
| 32 | pub exchange: String, // host name, trailing dot stripped |
| 33 | } |
| 34 | |
| 35 | /// The first nameserver listed in `/etc/resolv.conf`, or `8.8.8.8` where the |
| 36 | /// file is missing or unreadable. |
| 37 | pub fn system_resolver() -> SocketAddr { |
| 38 | let fallback = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 53); |
| 39 | let contents = match std::fs::read_to_string("/etc/resolv.conf") { |
| 40 | Ok(s) => s, |
| 41 | Err(_) => return fallback, |
| 42 | }; |
| 43 | for line in contents.lines() { |
| 44 | let trimmed = line.trim(); |
| 45 | if trimmed.starts_with('#') || trimmed.is_empty() { |
| 46 | continue; |
| 47 | } |
| 48 | if let Some(rest) = trimmed.strip_prefix("nameserver") { |
| 49 | let ip_str = rest.trim(); |
| 50 | if let Ok(ip) = ip_str.parse::<IpAddr>() { |
| 51 | return SocketAddr::new(ip, 53); |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | fallback |
| 56 | } |
| 57 | |
| 58 | /// Sorted into preference order, lowest first. A successful query that returns |
| 59 | /// no records falls back to a synthetic MX pointing at `domain` itself, per |
| 60 | /// RFC 5321 §5.1. |
| 61 | pub fn lookup_mx(domain: &str) -> Outcome<Vec<MxRecord>> { |
| 62 | let resolver = system_resolver(); |
| 63 | let response = res!(query(domain, QTYPE_MX, resolver)); |
| 64 | let mut records = res!(parse_mx_response(&response)); |
| 65 | if records.is_empty() { |
| 66 | records.push(MxRecord { |
| 67 | preference: 0, |
| 68 | exchange: domain.trim_end_matches('.').to_string(), |
| 69 | }); |
| 70 | } |
| 71 | records.sort(); |
| 72 | Ok(records) |
| 73 | } |
| 74 | |
| 75 | /// Every IPv4 answer, in the order the server sent them. |
| 76 | pub fn lookup_a(host: &str) -> Outcome<Vec<Ipv4Addr>> { |
| 77 | let resolver = system_resolver(); |
| 78 | let response = res!(query(host, QTYPE_A, resolver)); |
| 79 | parse_a_response(&response) |
| 80 | } |
| 81 | |
| 82 | |
| 83 | // ┌───────────────────────────────────────────────────────────────────────────┐ |
| 84 | // │ DNS WIRE FORMAT │ |
| 85 | // └───────────────────────────────────────────────────────────────────────────┘ |
| 86 | |
| 87 | const QTYPE_A: u16 = 1; |
| 88 | const QTYPE_MX: u16 = 15; |
| 89 | const QCLASS_IN: u16 = 1; |
| 90 | |
| 91 | const FLAG_RD: u16 = 0x0100; // Recursion desired. |
| 92 | |
| 93 | const RTYPE_A: u16 = 1; |
| 94 | const RTYPE_NS: u16 = 2; |
| 95 | const RTYPE_CNAME: u16 = 5; |
| 96 | const RTYPE_MX: u16 = 15; |
| 97 | |
| 98 | /// The raw response bytes. One retry on timeout, and no more. |
| 99 | fn query(name: &str, qtype: u16, resolver: SocketAddr) -> Outcome<Vec<u8>> { |
| 100 | let id: u16 = (std::process::id() as u16) ^ (qtype as u16); |
| 101 | let mut packet = Vec::with_capacity(64); |
| 102 | packet.extend_from_slice(&id.to_be_bytes()); |
| 103 | packet.extend_from_slice(&FLAG_RD.to_be_bytes()); |
| 104 | packet.extend_from_slice(&1u16.to_be_bytes()); // QDCOUNT |
| 105 | packet.extend_from_slice(&0u16.to_be_bytes()); // ANCOUNT |
| 106 | packet.extend_from_slice(&0u16.to_be_bytes()); // NSCOUNT |
| 107 | packet.extend_from_slice(&0u16.to_be_bytes()); // ARCOUNT |
| 108 | encode_qname(name, &mut packet); |
| 109 | packet.extend_from_slice(&qtype.to_be_bytes()); |
| 110 | packet.extend_from_slice(&QCLASS_IN.to_be_bytes()); |
| 111 | |
| 112 | let socket = match UdpSocket::bind("0.0.0.0:0") { |
| 113 | Ok(s) => s, |
| 114 | Err(e) => return Err(err!(e, |
| 115 | "Binding UDP socket for DNS query."; IO, Network, Init)), |
| 116 | }; |
| 117 | if let Err(e) = socket.set_read_timeout(Some(Duration::from_secs(5))) { |
| 118 | return Err(err!(e, |
| 119 | "Setting DNS read timeout."; IO, Network, Init)); |
| 120 | } |
| 121 | if let Err(e) = socket.send_to(&packet, resolver) { |
| 122 | return Err(err!(e, |
| 123 | "Sending DNS query to {:?}.", resolver; IO, Network, Write)); |
| 124 | } |
| 125 | let mut buf = [0u8; 4096]; |
| 126 | let (n, _src) = match socket.recv_from(&mut buf) { |
| 127 | Ok(p) => p, |
| 128 | Err(_) => { |
| 129 | // One retry. |
| 130 | if let Err(e) = socket.send_to(&packet, resolver) { |
| 131 | return Err(err!(e, |
| 132 | "Resending DNS query to {:?}.", resolver; |
| 133 | IO, Network, Write)); |
| 134 | } |
| 135 | match socket.recv_from(&mut buf) { |
| 136 | Ok(p) => p, |
| 137 | Err(e) => return Err(err!(e, |
| 138 | "Reading DNS response from {:?}.", resolver; |
| 139 | IO, Network, Read)), |
| 140 | } |
| 141 | } |
| 142 | }; |
| 143 | Ok(buf[..n].to_vec()) |
| 144 | } |
| 145 | |
| 146 | /// Length-prefixed labels, then a zero terminator. |
| 147 | fn encode_qname(name: &str, out: &mut Vec<u8>) { |
| 148 | for label in name.trim_end_matches('.').split('.') { |
| 149 | let bytes = label.as_bytes(); |
| 150 | out.push(bytes.len() as u8); |
| 151 | out.extend_from_slice(bytes); |
| 152 | } |
| 153 | out.push(0); |
| 154 | } |
| 155 | |
| 156 | fn parse_mx_response(buf: &[u8]) -> Outcome<Vec<MxRecord>> { |
| 157 | let (_id, ancount, mut pos) = res!(parse_response_header(buf)); |
| 158 | let mut out = Vec::new(); |
| 159 | for _ in 0..ancount { |
| 160 | let (rtype, rdlength, rdata_pos, next) = res!(parse_rr(buf, pos)); |
| 161 | pos = next; |
| 162 | if rtype == RTYPE_MX { |
| 163 | if rdlength < 3 { |
| 164 | return Err(err!( |
| 165 | "MX RDATA too short ({} bytes).", rdlength; |
| 166 | Invalid, Input, Decode)); |
| 167 | } |
| 168 | let pref = u16::from_be_bytes([ |
| 169 | buf[rdata_pos], |
| 170 | buf[rdata_pos + 1], |
| 171 | ]); |
| 172 | let (name, _consumed) = res!(read_name(buf, rdata_pos + 2)); |
| 173 | out.push(MxRecord { |
| 174 | preference: pref, |
| 175 | exchange: name, |
| 176 | }); |
| 177 | } |
| 178 | } |
| 179 | Ok(out) |
| 180 | } |
| 181 | |
| 182 | fn parse_a_response(buf: &[u8]) -> Outcome<Vec<Ipv4Addr>> { |
| 183 | let (_id, ancount, mut pos) = res!(parse_response_header(buf)); |
| 184 | let mut out = Vec::new(); |
| 185 | for _ in 0..ancount { |
| 186 | let (rtype, rdlength, rdata_pos, next) = res!(parse_rr(buf, pos)); |
| 187 | pos = next; |
| 188 | if rtype == RTYPE_A { |
| 189 | if rdlength != 4 { |
| 190 | return Err(err!( |
| 191 | "A RDATA must be 4 bytes (got {}).", rdlength; |
| 192 | Invalid, Input, Decode)); |
| 193 | } |
| 194 | out.push(Ipv4Addr::new( |
| 195 | buf[rdata_pos], |
| 196 | buf[rdata_pos + 1], |
| 197 | buf[rdata_pos + 2], |
| 198 | buf[rdata_pos + 3], |
| 199 | )); |
| 200 | } |
| 201 | // CNAME and NS records are silently skipped: lookup_a returns |
| 202 | // any direct A answers in the same response, which is what |
| 203 | // resolvers typically include. |
| 204 | let _ = RTYPE_CNAME; |
| 205 | let _ = RTYPE_NS; |
| 206 | } |
| 207 | Ok(out) |
| 208 | } |
| 209 | |
| 210 | /// `(transaction_id, ancount, position_of_first_answer)`, having read the |
| 211 | /// 12-byte header and the question section. |
| 212 | fn parse_response_header(buf: &[u8]) -> Outcome<(u16, u16, usize)> { |
| 213 | if buf.len() < 12 { |
| 214 | return Err(err!( |
| 215 | "DNS response too short ({} bytes).", buf.len(); |
| 216 | Invalid, Input, Decode)); |
| 217 | } |
| 218 | let id = u16::from_be_bytes([buf[0], buf[1]]); |
| 219 | let flags = u16::from_be_bytes([buf[2], buf[3]]); |
| 220 | let qdcount = u16::from_be_bytes([buf[4], buf[5]]); |
| 221 | let ancount = u16::from_be_bytes([buf[6], buf[7]]); |
| 222 | // Bottom 4 bits of the flags are RCODE. |
| 223 | let rcode = (flags & 0x000f) as u8; |
| 224 | if rcode != 0 && rcode != 3 { |
| 225 | return Err(err!( |
| 226 | "DNS response RCODE = {}.", rcode; |
| 227 | IO, Network, Wire)); |
| 228 | } |
| 229 | let mut pos = 12; |
| 230 | for _ in 0..qdcount { |
| 231 | let (_qname, consumed) = res!(read_name(buf, pos)); |
| 232 | pos = consumed + 4; // skip QTYPE + QCLASS |
| 233 | } |
| 234 | Ok((id, ancount, pos)) |
| 235 | } |
| 236 | |
| 237 | /// One resource record header at `pos`, as |
| 238 | /// `(rtype, rdlength, rdata_position, position_after_rdata)`. |
| 239 | fn parse_rr(buf: &[u8], pos: usize) -> Outcome<(u16, u16, usize, usize)> { |
| 240 | let (_name, after_name) = res!(read_name(buf, pos)); |
| 241 | if after_name + 10 > buf.len() { |
| 242 | return Err(err!( |
| 243 | "DNS RR header truncated at offset {}.", after_name; |
| 244 | Invalid, Input, Decode)); |
| 245 | } |
| 246 | let rtype = u16::from_be_bytes([buf[after_name], buf[after_name + 1]]); |
| 247 | let _rclass = u16::from_be_bytes([buf[after_name + 2], buf[after_name + 3]]); |
| 248 | let _ttl = u32::from_be_bytes([ |
| 249 | buf[after_name + 4], |
| 250 | buf[after_name + 5], |
| 251 | buf[after_name + 6], |
| 252 | buf[after_name + 7], |
| 253 | ]); |
| 254 | let rdlength = u16::from_be_bytes([buf[after_name + 8], buf[after_name + 9]]); |
| 255 | let rdata_pos = after_name + 10; |
| 256 | let after_rdata = rdata_pos + rdlength as usize; |
| 257 | if after_rdata > buf.len() { |
| 258 | return Err(err!( |
| 259 | "DNS RR RDATA truncated."; |
| 260 | Invalid, Input, Decode)); |
| 261 | } |
| 262 | Ok((rtype, rdlength, rdata_pos, after_rdata)) |
| 263 | } |
| 264 | |
| 265 | /// Follows the compression pointers of RFC 1035 §4.1.4. The position returned is |
| 266 | /// the one after the *uncompressed* part of the name, which is where the caller |
| 267 | /// resumes reading. |
| 268 | fn read_name(buf: &[u8], start: usize) -> Outcome<(String, usize)> { |
| 269 | let mut name = String::new(); |
| 270 | let mut pos = start; |
| 271 | let mut after: Option<usize> = None; |
| 272 | let mut hops = 0; |
| 273 | loop { |
| 274 | if hops > 20 { |
| 275 | return Err(err!( |
| 276 | "DNS name compression loop."; |
| 277 | Invalid, Input, Decode)); |
| 278 | } |
| 279 | if pos >= buf.len() { |
| 280 | return Err(err!( |
| 281 | "DNS name overruns buffer."; |
| 282 | Invalid, Input, Decode)); |
| 283 | } |
| 284 | let len = buf[pos]; |
| 285 | if len == 0 { |
| 286 | pos += 1; |
| 287 | if after.is_none() { |
| 288 | after = Some(pos); |
| 289 | } |
| 290 | break; |
| 291 | } |
| 292 | if len & 0xc0 == 0xc0 { |
| 293 | // Compression pointer. |
| 294 | if pos + 1 >= buf.len() { |
| 295 | return Err(err!( |
| 296 | "DNS pointer truncated."; |
| 297 | Invalid, Input, Decode)); |
| 298 | } |
| 299 | let target = (((len & 0x3f) as usize) << 8) | (buf[pos + 1] as usize); |
| 300 | if after.is_none() { |
| 301 | after = Some(pos + 2); |
| 302 | } |
| 303 | pos = target; |
| 304 | hops += 1; |
| 305 | continue; |
| 306 | } |
| 307 | if len & 0xc0 != 0 { |
| 308 | return Err(err!( |
| 309 | "DNS label length has reserved high bits set."; |
| 310 | Invalid, Input, Decode)); |
| 311 | } |
| 312 | let label_end = pos + 1 + len as usize; |
| 313 | if label_end > buf.len() { |
| 314 | return Err(err!( |
| 315 | "DNS label overruns buffer."; |
| 316 | Invalid, Input, Decode)); |
| 317 | } |
| 318 | if !name.is_empty() { |
| 319 | name.push('.'); |
| 320 | } |
| 321 | name.push_str(&String::from_utf8_lossy(&buf[pos + 1..label_end])); |
| 322 | pos = label_end; |
| 323 | } |
| 324 | Ok((name, after.unwrap_or(pos))) |
| 325 | } |