Oregami
Repositories/oxedyne/fe2o3

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
15use oxedyne_fe2o3_core::prelude::*;
16
17use 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)]
30pub 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.
37pub 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.
61pub 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.
76pub 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
87const QTYPE_A: u16 = 1;
88const QTYPE_MX: u16 = 15;
89const QCLASS_IN: u16 = 1;
90
91const FLAG_RD: u16 = 0x0100; // Recursion desired.
92
93const RTYPE_A: u16 = 1;
94const RTYPE_NS: u16 = 2;
95const RTYPE_CNAME: u16 = 5;
96const RTYPE_MX: u16 = 15;
97
98/// The raw response bytes. One retry on timeout, and no more.
99fn 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.
147fn 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
156fn 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
182fn 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.
212fn 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)`.
239fn 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.
268fn 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}