dnsguide

repository·master·Indexed 26 days ago

https://github.com/emilhernvall/dnsguide

An educational project that guides developers through building a full DNS server in Rust. The guide covers the DNS protocol from first principles, including the implementation of a BytePacketBuffer for packet manipulation, a stub resolver, support for various record types (A, NS, CNAME, MX, AAAA), and the progression toward a recursive resolver.

Tokens
7.6K
Snippets
13
Records
24
Agent score
89%

What's inside dnsguide

  1. Overview of dnsguide

    master
    dnsguide is a project designed to teach the implementation of a DNS server in Rust from first principles. It provides a step-by-step guide through the DNS protocol, building a stub resolver, adding record types, and implementing a recursive resolver.
  2. Understand DNS Server Roles: Authoritative vs Caching

    master

    DNS servers typically fall into two categories:

    • Authoritative Server: Hosts specific DNS zones (e.g., ns1.google.com for google.com). It only responds to queries regarding the zones it hosts. If a query has the RD (Recursion Desired) flag set, an authoritative server will typically return an error.
    • Caching Server: Services lookups by checking a local cache first, then performing recursive lookups if necessary (e.g., Google's 8.8.8.8). These servers set the RA (Recursion Available) flag in responses.

    Key Flags:

    • RD (Recursion Desired): Set by a client (stub resolver) to request recursion.
    • RA (Recursion Available): Set by a server to indicate it supports recursive lookups.
  3. Implement a DNS stub resolver

    master

    A stub resolver is a DNS client that sends queries to a recursive DNS server. To implement one in Rust:

    1. Create a DnsPacket: Initialize a new packet and set the recursion_desired flag to true in the header.
    2. Add a Question: Push a DnsQuestion (containing the target domain and QueryType) into the packet's questions vector.
    3. Serialize: Use packet.write(&mut buffer) to write the packet into a BytePacketBuffer.
    4. Send via UDP: Use std::net::UdpSocket to send the buffer contents to the target DNS server (e.g., 8.8.8.8:53).
    5. Receive Response: Use socket.recv_from to read the response into a buffer, then use DnsPacket::from_buffer to parse the received bytes back into a DnsPacket structure.
    fn main() -> Result<()> {
        let qname = "google.com";
        let qtype = QueryType::A;
        let server = ("8.8.8.8", 53);
    
        let socket = UdpSocket::bind(("0.0.0.0", 43210))?;
    
        let mut packet = DnsPacket::new();
        packet.header.id = 6666;
        packet.header.questions = 1;
        packet.header.recursion_desired = true;
        packet.questions.push(DnsQuestion::new(qname.to_string(), qtype));
    
        let mut req_buffer = BytePacketBuffer::new();
        packet.write(&mut req_buffer)?;
    
        socket.send_to(&req_buffer.buf[0..req_buffer.pos], server)?;
    
        let mut res_buffer = BytePacketBuffer::new();
        socket.recv_from(&mut res_buffer.buf)?;
    
        let res_packet = DnsPacket::from_buffer(&mut res_buffer)?;
        println!("{:#?}", res_packet.header);
        // ... print questions, answers, etc.
        Ok(())
    }
  4. Implement a DNS Proxy Server in Rust

    master

    A DNS proxy server receives queries, forwards them to an upstream caching server (like 8.8.8.8), and returns the result to the client.

    To implement this, you need to:

    1. Bind a UdpSocket to a port (e.g., 2053).
    2. In a loop, use socket.recv_from to capture incoming packets and the sender's address (src).
    3. Parse the packet using DnsPacket::from_buffer.
    4. Extract the question and use a lookup function to forward the query to an upstream server.
    5. Construct a response DnsPacket with the same id, set recursion_desired and recursion_available to true, and set response to true.
    6. Copy the answers, authorities, and resources from the upstream result into your response packet.
    7. Handle errors by setting rescode to SERVFAIL (if lookup fails) or FORMERR (if no question is present).
    8. Encode the response with packet.write and send it back to src using socket.send_to.
    /// Handle a single incoming packet
    fn handle_query(socket: &UdpSocket) -> Result<()> {
        let mut req_buffer = BytePacketBuffer::new();
        let (_, src) = socket.recv_from(&mut req_buffer.buf)?;
        let mut request = DnsPacket::from_buffer(&mut req_buffer)?;
    
        let mut packet = DnsPacket::new();
        packet.header.id = request.header.id;
        packet.header.recursion_desired = true;
        packet.header.recursion_available = true;
        packet.header.response = true;
    
        if let Some(question) = request.questions.pop() {
            if let Ok(result) = lookup(&question.name, question.qtype) {
                packet.questions.push(question);
                packet.header.rescode = result.header.rescode;
    
                for rec in result.answers { packet.answers.push(rec); }
                for rec in result.authorities { packet.authorities.push(rec); }
                for rec in result.resources { packet.resources.push(rec); }
            } else {
                packet.header.rescode = ResultCode::SERVFAIL;
            }
        } else {
            packet.header.rescode = ResultCode::FORMERR;
        }
    
        let mut res_buffer = BytePacketBuffer::new();
        packet.write(&mut res_buffer)?;
        let len = res_buffer.pos();
        let data = res_buffer.get_range(0, len)?;
        socket.send_to(data, src)?;
    
        Ok()
    }
    
    fn main() -> Result<()> {
        let socket = UdpSocket::bind(("0.0.0.0", 2053))?;
        loop {
            if let Err(e) = handle_query(&socket) {
                eprintln!("An error occurred: {}", e);
            }
        }
    }
  5. Extend DnsRecord for writing new record types

    master

    To implement writing for new record types, extend the DnsRecord::write method. For records containing variable-length label sequences (NS, CNAME, MX), use a placeholder (writing a zero size) at the start of the sequence, write the actual data, and then use BytePacketBuffer::set_u16 to go back and fill in the correct size.

    impl DnsRecord {
        pub fn write(&self, buffer: &mut BytePacketBuffer) -> Result<usize> {
            let start_pos = buffer.pos();
    
            match *self {
                DnsRecord::A { ref domain, ref addr, ttl } => {
                    buffer.write_qname(domain)?;
                    buffer.write_u16(QueryType::A.to_num())?;
                    buffer.write_u16(1)?;
                    buffer.write_u32(ttl)?;
                    buffer.write_u16(4)?;
    
                    let octets = addr.octets();
                    buffer.write_u8(octets[0])?;
                    buffer.write_u8(octets[1])?;
                    buffer.write_u8(octets[2])?;
                    buffer.write_u8(octets[3])?;
                }
                DnsRecord::NS { ref domain, ref host, ttl } => {
                    buffer.write_qname(domain)?;
                    buffer.write_u16(QueryType::NS.to_num())?;
                    buffer.write_u16(1)?;
                    buffer.write_u32(ttl)?;
    
                    let pos = buffer.pos();
                    buffer.write_u16(0)?;
    
                    buffer.write_qname(host)?;
    
                    let size = buffer.pos() - (pos + 2);
                    buffer.set_u16(pos, size as u16)?;
                }
                // ... other variants (CNAME, MX, AAAA, UNKNOWN)
            }
    
            Ok(buffer.pos() - start_pos)
        }
    }
  6. Parse DNS domain names with label compression

    master
    DNS domain names are encoded as a sequence of labels. Each label is preceded by a length byte. To save space, DNS uses compression: if a label's two most significant bits are set (0xC0), the remaining 14 bits represent an offset (jump) to a previous occurrence of the name in the packet.
  7. Inspect DNS packets using dig, netcat, and hexdump

    master

    You can manually capture and inspect DNS traffic to understand the protocol structure.

    1. Listen for a query: Use nc (netcat) to listen on a specific UDP port and redirect the incoming packet to a file.
    2. Send a query: Use dig with the +noedns flag (to avoid eDNS extensions and stick to the original 512-byte limit) and point it to your local listener.
    3. Capture a response: Use nc to send the captured query to a real DNS server (like 8.8.8.8) and redirect the response to a file.
    4. Inspect bytes: Use hexdump -C to view the raw hex and ASCII representation of the packets.
  8. Extend QueryType with more record types

    master

    To support additional DNS record types, update the QueryType enum and implement the to_num and from_num utility functions to handle the mapping between enum variants and their numeric IDs.

    Supported types in this guide:

    • A (1)
    • NS (2)
    • CNAME (5)
    • MX (15)
    • AAAA (28)
    • UNKNOWN(u16) (fallback)
    #[derive(PartialEq, Eq, Debug, Clone, Hash, Copy)]
    pub enum QueryType {
        UNKNOWN(u16),
        A,     // 1
        NS,    // 2
        CNAME, // 5
        MX,    // 15
        AAAA,  // 28,
    }
    
    impl QueryType {
        pub fn to_num(&self) -> u16 {
            match *self {
                QueryType::UNKNOWN(x) => x,
                QueryType::A => 1,
                QueryType::NS => 2,
                QueryType::CNAME => 5,
                QueryType::MX => 15,
                QueryType::AAAA => 28,
            }
        }
    
        pub fn from_num(num: u16) -> QueryType {
            match num {
                1 => QueryType::A,
                2 => QueryType::NS,
                5 => QueryType::CNAME,
                15 => QueryType::MX,
                28 => QueryType::AAAA,
                _ => QueryType::UNKNOWN(num),
            }
        }
    }
  9. Extend DnsRecord for reading new record types

    master

    To implement reading for new record types, update the DnsRecord enum to include the new variants and extend the DnsRecord::read method. The method should use a BytePacketBuffer to parse the preamble (domain, qtype, class, ttl, and data_len) and then match on the QueryType to parse the specific record data.

    Key parsing logic for common types:

    • A: Reads 4 bytes for Ipv4Addr.
    • AAAA: Reads 16 bytes for Ipv6Addr.
    • NS/CNAME: Reads a qname for the host.
    • MX: Reads a 2-byte priority followed by a qname for the host.
    • UNKNOWN: Uses buffer.step(data_len) to skip the data.
    impl DnsRecord {
        pub fn read(buffer: &mut BytePacketBuffer) -> Result<DnsRecord> {
            let mut domain = String::new();
            buffer.read_qname(&mut domain)?;
    
            let qtype_num = buffer.read_u16()?;
            let qtype = QueryType::from_num(qtype_num);
            let _ = buffer.read_u16()?;
            let ttl = buffer.read_u32()?;
            let data_len = buffer.read_u16()?;
    
            match qtype {
                QueryType::A => {
                    let raw_addr = buffer.read_u32()?;
                    let addr = Ipv4Addr::new(
                        ((raw_addr >> 24) & 0xFF) as u8,
                        ((raw_addr >> 16) & 0xFF) as u8,
                        ((raw_addr >> 8) & 0xFF) as u8,
                        ((raw_addr >> 0) & 0xFF) as u8,
                    );
    
                    Ok(DnsRecord::A {
                        domain: domain,
                        addr: addr,
                        ttl: ttl,
                    })
                }
                // ... other variants (AAAA, NS, CNAME, MX, UNKNOWN) 
            }
        }
    }
  10. Verify DNS Server Behavior with `dig`

    master

    You can use the dig utility to test how different DNS servers respond to queries and recursion flags:

    • Test a Caching Server: dig @8.8.8.8 yahoo.com (Should return results with ra flag).
    • Test an Authoritative Server (expecting refusal): dig @ns1.google.com yahoo.com (Should return status: REFUSED because yahoo.com is not in the google.com zone).
    • Test an Authoritative Server (for its own zone): dig @ns1.google.com google.com (Should return results, but may warn that recursion is unavailable).
    • Test without recursion flag: dig +norecurse @ns1.google.com google.com (Removes the recursion warning by explicitly unsetting the RD flag).
  11. Run DNS server implementation samples

    master

    The project provides incremental code samples for each chapter of the guide, named sample1.rs through sample5.rs. You can run these samples using cargo run --example followed by the sample name.

    cargo run --example sample1
  12. Test recursive lookup with dig

    master

    To verify your DNS server is performing recursive lookups correctly, use the dig command pointing to your local server's IP and port. For example, if your server is running on 127.0.0.1 on port 2053:

    dig @127.0.0.1 -p 2053 www.google.com

    A successful recursive lookup will return an ANSWER SECTION containing the requested domain's IP address, even though the server started from the root hints.

    # dig @127.0.0.1 -p 2053 www.google.com