Skip to main content

microdns/
lib.rs

1//! # microdns
2//!
3//! A minimal DNS resolver library using only the Rust standard library.
4//!
5//! This crate provides functionality to resolve various DNS record types,
6//! including MX records for mail servers and A/AAAA records for IP addresses.
7//!
8//! ## Example
9//!
10//! ```rust
11//! use microdns::{lookup_mx_records, lookup_ip_addresses, resolve_mx_server_ips};
12//!
13//! fn main() -> Result<(), microdns::Error> {
14//!     let domain = "google.com";
15//!     
16//!     // Get MX records
17//!     let mx_records = lookup_mx_records(domain)?;
18//!     
19//!     // Get IP addresses for a specific hostname
20//!     let ips = lookup_ip_addresses("smtp.google.com")?;
21//!     
22//!     // Or resolve all MX servers to their IPs
23//!     let server_ips = resolve_mx_server_ips(domain)?;
24//!     
25//!     Ok(())
26//! }
27//! ```
28
29use std::error;
30use std::fmt;
31use std::io;
32use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, UdpSocket};
33use std::time::Duration;
34
35/// Default DNS servers to try (in order)
36pub const DEFAULT_DNS_SERVERS: &[&str] = &[
37    "1.1.1.1", // Cloudflare primary (fastest)
38    "1.0.0.1", // Cloudflare secondary
39    "8.8.8.8", // Google primary
40    "8.8.4.4", // Google secondary
41];
42
43/// DNS record type constants
44pub const DNS_TYPE_A: u16 = 1; // A record (IPv4 address)
45pub const DNS_TYPE_MX: u16 = 15; // MX record (mail exchange)
46pub const DNS_TYPE_AAAA: u16 = 28; // AAAA record (IPv6 address)
47
48/// Error type for DNS operations
49#[derive(Debug)]
50pub enum Error {
51    /// An IO error occurred
52    Io(io::Error),
53    /// DNS query timed out
54    Timeout,
55    /// DNS server returned an error code
56    ServerError(u16),
57    /// No records found for the requested domain/type
58    NoRecordsFound,
59    /// Malformed DNS packet
60    MalformedPacket,
61}
62
63impl fmt::Display for Error {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Error::Io(err) => write!(f, "IO error: {}", err),
67            Error::Timeout => write!(f, "DNS query timed out"),
68            Error::ServerError(code) => write!(f, "DNS server returned error code: {}", code),
69            Error::NoRecordsFound => write!(f, "No DNS records found"),
70            Error::MalformedPacket => write!(f, "Malformed DNS packet"),
71        }
72    }
73}
74
75impl error::Error for Error {
76    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
77        match self {
78            Error::Io(err) => Some(err),
79            _ => None,
80        }
81    }
82}
83
84impl From<io::Error> for Error {
85    fn from(err: io::Error) -> Self {
86        Error::Io(err)
87    }
88}
89
90/// DNS header structure
91#[derive(Debug)]
92pub struct DnsHeader {
93    /// Transaction ID
94    pub id: u16,
95    /// Flags field
96    pub flags: u16,
97    /// Number of questions
98    pub questions: u16,
99    /// Number of answer records
100    pub answers: u16,
101    /// Number of authority records
102    pub authorities: u16,
103    /// Number of additional records
104    pub additionals: u16,
105}
106
107/// Parse DNS header from buffer
108pub fn parse_dns_header(buffer: &[u8]) -> Result<DnsHeader, Error> {
109    if buffer.len() < 12 {
110        return Err(Error::MalformedPacket);
111    }
112
113    let header = DnsHeader {
114        id: u16::from_be_bytes([buffer[0], buffer[1]]),
115        flags: u16::from_be_bytes([buffer[2], buffer[3]]),
116        questions: u16::from_be_bytes([buffer[4], buffer[5]]),
117        answers: u16::from_be_bytes([buffer[6], buffer[7]]),
118        authorities: u16::from_be_bytes([buffer[8], buffer[9]]),
119        additionals: u16::from_be_bytes([buffer[10], buffer[11]]),
120    };
121
122    // Check response code (lower 4 bits of flags)
123    let rcode = header.flags & 0x0F;
124    if rcode != 0 {
125        return Err(Error::ServerError(rcode));
126    }
127
128    Ok(header)
129}
130
131/// Skip DNS question section
132pub fn skip_question(buffer: &[u8], mut pos: usize) -> Result<usize, Error> {
133    if pos >= buffer.len() {
134        return Err(Error::MalformedPacket);
135    }
136
137    // Skip name
138    while pos < buffer.len() {
139        let len = buffer[pos] as usize;
140        if len == 0 {
141            pos += 1;
142            break;
143        }
144
145        // Handle compression pointers (0xC0 mask)
146        if len & 0xC0 == 0xC0 {
147            pos += 2;
148            break;
149        }
150
151        pos += len + 1;
152
153        if pos >= buffer.len() {
154            return Err(Error::MalformedPacket);
155        }
156    }
157
158    // Skip type and class (4 bytes)
159    if pos + 4 > buffer.len() {
160        return Err(Error::MalformedPacket);
161    }
162
163    Ok(pos + 4)
164}
165
166/// DNS record data variants
167#[derive(Debug, Clone)]
168pub enum RecordData {
169    /// MX record with priority and server hostname
170    MX {
171        /// Priority value (lower is preferred)
172        priority: u16,
173        /// Server hostname
174        server: String,
175    },
176    /// A record with IPv4 address
177    A(Ipv4Addr),
178    /// AAAA record with IPv6 address
179    AAAA(Ipv6Addr),
180    /// Unknown record type
181    Unknown,
182}
183
184/// DNS record structure
185#[derive(Debug)]
186pub struct DnsRecord {
187    /// DNS record type
188    pub record_type: u16,
189    /// Record data based on type
190    pub data: RecordData,
191}
192
193/// Parse DNS answer record
194pub fn parse_answer(buffer: &[u8], mut pos: usize) -> Result<(DnsRecord, usize), Error> {
195    if pos >= buffer.len() {
196        return Err(Error::MalformedPacket);
197    }
198
199    // Skip name field
200    while pos < buffer.len() {
201        let len = buffer[pos] as usize;
202        if len == 0 {
203            pos += 1;
204            break;
205        }
206
207        // Handle compression pointers
208        if len & 0xC0 == 0xC0 {
209            pos += 2;
210            break;
211        }
212
213        pos += len + 1;
214
215        if pos >= buffer.len() {
216            return Err(Error::MalformedPacket);
217        }
218    }
219
220    // Ensure we have enough bytes for the record header
221    if pos + 10 > buffer.len() {
222        return Err(Error::MalformedPacket);
223    }
224
225    // Parse record type and class
226    let record_type = u16::from_be_bytes([buffer[pos], buffer[pos + 1]]);
227    pos += 4; // Skip type and class
228
229    // Skip TTL (4 bytes)
230    pos += 4;
231
232    // Get data length
233    let data_len = u16::from_be_bytes([buffer[pos], buffer[pos + 1]]) as usize;
234    pos += 2;
235
236    // Ensure we have enough bytes for the record data
237    if pos + data_len > buffer.len() {
238        return Err(Error::MalformedPacket);
239    }
240
241    let data = match record_type {
242        DNS_TYPE_MX => {
243            if data_len < 2 {
244                return Err(Error::MalformedPacket);
245            }
246
247            let priority = u16::from_be_bytes([buffer[pos], buffer[pos + 1]]);
248            pos += 2;
249
250            // Extract hostname (rest of data)
251            let server = parse_dns_name(buffer, pos)?;
252            pos += data_len - 2; // -2 because we already processed priority
253
254            RecordData::MX { priority, server }
255        }
256        DNS_TYPE_A => {
257            if data_len != 4 {
258                return Err(Error::MalformedPacket);
259            }
260
261            let ipv4 = Ipv4Addr::new(
262                buffer[pos],
263                buffer[pos + 1],
264                buffer[pos + 2],
265                buffer[pos + 3],
266            );
267            pos += 4;
268            RecordData::A(ipv4)
269        }
270        DNS_TYPE_AAAA => {
271            if data_len != 16 {
272                return Err(Error::MalformedPacket);
273            }
274
275            let mut ipv6_bytes = [0u8; 16];
276            ipv6_bytes.copy_from_slice(&buffer[pos..pos + 16]);
277            let ipv6 = Ipv6Addr::from(ipv6_bytes);
278            pos += 16;
279            RecordData::AAAA(ipv6)
280        }
281        _ => {
282            // Skip unknown record types
283            pos += data_len;
284            RecordData::Unknown
285        }
286    };
287
288    Ok((DnsRecord { record_type, data }, pos))
289}
290
291/// Parse DNS name from buffer
292pub fn parse_dns_name(buffer: &[u8], mut pos: usize) -> Result<String, Error> {
293    if pos >= buffer.len() {
294        return Err(Error::MalformedPacket);
295    }
296
297    let mut name = String::new();
298    let mut first = true;
299
300    // Detect compression loops
301    let mut jumps = 0;
302    const MAX_JUMPS: usize = 10; // Prevent infinite loops
303
304    loop {
305        if pos >= buffer.len() {
306            return Err(Error::MalformedPacket);
307        }
308
309        let len = buffer[pos] as usize;
310        pos += 1;
311
312        // End of name
313        if len == 0 {
314            break;
315        }
316
317        // Handle compression pointers
318        if len & 0xC0 == 0xC0 {
319            if pos >= buffer.len() {
320                return Err(Error::MalformedPacket);
321            }
322
323            jumps += 1;
324            if jumps > MAX_JUMPS {
325                return Err(Error::MalformedPacket);
326            }
327
328            let offset = ((len & 0x3F) << 8) | buffer[pos] as usize;
329
330            // If this is not the first part, add a dot
331            if !first {
332                name.push('.');
333            }
334
335            // Append the name from the offset position
336            let remainder = parse_dns_name(buffer, offset)?;
337            name.push_str(&remainder);
338            break;
339        }
340
341        // Regular label
342        if pos + len > buffer.len() {
343            return Err(Error::MalformedPacket);
344        }
345
346        // If this is not the first part, add a dot
347        if !first {
348            name.push('.');
349        }
350        first = false;
351
352        // Append this part
353        name.push_str(&String::from_utf8_lossy(&buffer[pos..pos + len]));
354        pos += len;
355    }
356
357    Ok(name)
358}
359
360/// MX record structure
361#[derive(Debug, Clone, PartialEq)]
362pub struct MxRecord {
363    /// Priority value (lower is preferred)
364    pub priority: u16,
365    /// Server hostname
366    pub server: String,
367}
368
369/// Server with resolved IP addresses
370#[derive(Debug, Clone, PartialEq)]
371pub struct ServerIpRecord {
372    /// Server hostname
373    pub server: String,
374    /// List of IP addresses (v4 and v6)
375    pub ip_addresses: Vec<IpAddr>,
376}
377
378/// DNS query configuration
379#[derive(Debug, Clone)]
380pub struct DnsConfig {
381    /// List of DNS servers to try
382    pub servers: Vec<String>,
383    /// Query timeout in seconds
384    pub timeout: u64,
385}
386
387impl Default for DnsConfig {
388    fn default() -> Self {
389        Self {
390            servers: DEFAULT_DNS_SERVERS.iter().map(|s| s.to_string()).collect(),
391            timeout: 5,
392        }
393    }
394}
395
396/// Build DNS query packet
397pub fn build_dns_query(domain: &str, record_type: u16) -> Result<Vec<u8>, Error> {
398    let mut packet = Vec::new();
399
400    // DNS header (12 bytes)
401    let id = 1_i16;
402    packet.extend_from_slice(&id.to_be_bytes());
403    packet.extend_from_slice(&[0x01, 0x00]); // Flags: standard query
404    packet.extend_from_slice(&[0x00, 0x01]); // Questions: 1
405    packet.extend_from_slice(&[0x00, 0x00]); // Answer RRs: 0
406    packet.extend_from_slice(&[0x00, 0x00]); // Authority RRs: 0
407    packet.extend_from_slice(&[0x00, 0x00]); // Additional RRs: 0
408
409    // Encode domain name in DNS format
410    for part in domain.split('.') {
411        if part.is_empty() {
412            continue;
413        }
414
415        if part.len() > 63 {
416            return Err(Error::MalformedPacket);
417        }
418
419        packet.push(part.len() as u8);
420        packet.extend_from_slice(part.as_bytes());
421    }
422    packet.push(0); // Terminating zero byte
423
424    // Query type and class (IN = 1)
425    packet.extend_from_slice(&record_type.to_be_bytes());
426    packet.extend_from_slice(&[0x00, 0x01]); // Class: IN
427
428    Ok(packet)
429}
430
431/// Parse MX records from DNS response
432pub fn parse_mx_records(buffer: &[u8]) -> Result<Vec<MxRecord>, Error> {
433    let mut records = Vec::new();
434    let header = parse_dns_header(buffer)?;
435
436    if header.answers == 0 {
437        return Err(Error::NoRecordsFound);
438    }
439
440    // Skip question section
441    let mut pos = 12; // Header size
442    for _ in 0..header.questions {
443        pos = skip_question(buffer, pos)?;
444    }
445
446    // Parse answer section
447    for _ in 0..header.answers {
448        let (record, new_pos) = parse_answer(buffer, pos)?;
449        pos = new_pos;
450
451        if record.record_type == DNS_TYPE_MX {
452            if let RecordData::MX { priority, server } = record.data {
453                records.push(MxRecord { priority, server });
454            }
455        }
456    }
457
458    if records.is_empty() {
459        return Err(Error::NoRecordsFound);
460    }
461
462    // Sort by priority (lower is preferred)
463    records.sort_by_key(|r| r.priority);
464    Ok(records)
465}
466
467/// Parse IP records from DNS response
468pub fn parse_ip_records(buffer: &[u8]) -> Result<Vec<IpAddr>, Error> {
469    let mut ips = Vec::new();
470    let header = parse_dns_header(buffer)?;
471
472    // Skip question section
473    let mut pos = 12; // Header size
474    for _ in 0..header.questions {
475        pos = skip_question(buffer, pos)?;
476    }
477
478    // Parse answer section
479    for _ in 0..header.answers {
480        let (record, new_pos) = parse_answer(buffer, pos)?;
481        pos = new_pos;
482
483        match record.data {
484            RecordData::A(ipv4) => ips.push(IpAddr::V4(ipv4)),
485            RecordData::AAAA(ipv6) => ips.push(IpAddr::V6(ipv6)),
486            _ => {}
487        }
488    }
489
490    if ips.is_empty() {
491        return Err(Error::NoRecordsFound);
492    }
493
494    Ok(ips)
495}
496
497/// Send DNS query and get response using the specified configuration
498pub fn lookup_dns_records(
499    domain: &str,
500    record_type: u16,
501    config: Option<DnsConfig>,
502) -> Result<Vec<u8>, Error> {
503    let config = config.unwrap_or_default();
504
505    // Build DNS query packet
506    let query = build_dns_query(domain, record_type)?;
507
508    // Try each DNS server until we get a response
509    for server in &config.servers {
510        let server_addr = format!("{}:53", server);
511
512        match try_dns_query(&server_addr, &query, config.timeout) {
513            Ok(response) => return Ok(response),
514            Err(Error::Timeout) | Err(Error::Io(_)) => continue, // Try next server
515            Err(e) => return Err(e),
516        }
517    }
518
519    Err(Error::Timeout)
520}
521
522/// Try a DNS query against a specific server
523fn try_dns_query(server: &str, query: &[u8], timeout: u64) -> Result<Vec<u8>, Error> {
524    // Create DNS socket
525    let socket = UdpSocket::bind("0.0.0.0:0")?;
526    socket.set_read_timeout(Some(Duration::from_secs(timeout)))?;
527
528    // Connect to DNS server
529    socket.connect(server)?;
530
531    // Send query
532    socket.send(query)?;
533
534    // Receive response
535    let mut buffer = [0; 512];
536    let size = socket.recv(&mut buffer)?;
537
538    Ok(buffer[..size].to_vec())
539}
540
541/// Lookup MX records for a domain
542pub fn lookup_mx_records(domain: &str) -> Result<Vec<MxRecord>, Error> {
543    lookup_mx_records_with_config(domain, None)
544}
545
546/// Lookup MX records for a domain with custom configuration
547pub fn lookup_mx_records_with_config(
548    domain: &str,
549    config: Option<DnsConfig>,
550) -> Result<Vec<MxRecord>, Error> {
551    let response = lookup_dns_records(domain, DNS_TYPE_MX, config)?;
552    parse_mx_records(&response)
553}
554
555/// Lookup IP addresses for a hostname
556pub fn lookup_ip_addresses(hostname: &str) -> Result<Vec<IpAddr>, Error> {
557    lookup_ip_addresses_with_config(hostname, None)
558}
559
560/// Lookup IP addresses for a hostname with custom configuration
561pub fn lookup_ip_addresses_with_config(
562    hostname: &str,
563    config: Option<DnsConfig>,
564) -> Result<Vec<IpAddr>, Error> {
565    let mut ips = Vec::new();
566
567    // Get A records (IPv4)
568    match lookup_dns_records(hostname, DNS_TYPE_A, config.clone()) {
569        Ok(response) => match parse_ip_records(&response) {
570            Ok(v4_ips) => ips.extend(v4_ips),
571            Err(Error::NoRecordsFound) => {} // Ignore if no records found
572            Err(e) => return Err(e),
573        },
574        Err(Error::NoRecordsFound) => {} // Ignore if no records found
575        Err(e) => return Err(e),
576    }
577
578    // Get AAAA records (IPv6)
579    match lookup_dns_records(hostname, DNS_TYPE_AAAA, config) {
580        Ok(response) => match parse_ip_records(&response) {
581            Ok(v6_ips) => ips.extend(v6_ips),
582            Err(Error::NoRecordsFound) => {} // Ignore if no records found
583            Err(e) => return Err(e),
584        },
585        Err(Error::NoRecordsFound) => {} // Ignore if no records found
586        Err(e) => return Err(e),
587    }
588
589    if ips.is_empty() {
590        return Err(Error::NoRecordsFound);
591    }
592
593    Ok(ips)
594}
595
596/// Resolve MX records to their IP addresses
597pub fn resolve_mx_server_ips(domain: &str) -> Result<Vec<ServerIpRecord>, Error> {
598    resolve_mx_server_ips_with_config(domain, None)
599}
600
601/// Resolve MX records to their IP addresses with custom configuration
602pub fn resolve_mx_server_ips_with_config(
603    domain: &str,
604    config: Option<DnsConfig>,
605) -> Result<Vec<ServerIpRecord>, Error> {
606    let mx_records = lookup_mx_records_with_config(domain, config.clone())?;
607
608    let mut server_ips = Vec::new();
609    for mx in mx_records {
610        match lookup_ip_addresses_with_config(&mx.server, config.clone()) {
611            Ok(ips) => {
612                server_ips.push(ServerIpRecord {
613                    server: mx.server,
614                    ip_addresses: ips,
615                });
616            }
617            Err(Error::NoRecordsFound) => {} // Skip servers with no IP records
618            Err(e) => return Err(e),
619        }
620    }
621
622    if server_ips.is_empty() {
623        return Err(Error::NoRecordsFound);
624    }
625
626    Ok(server_ips)
627}