dnsguide
repository·master·Indexed 26 days ago
https://github.com/emilhernvall/dnsguideAn 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.
What's inside dnsguide
- 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.
Understand DNS Server Roles: Authoritative vs Caching
masterDNS servers typically fall into two categories:
- Authoritative Server: Hosts specific DNS zones (e.g.,
ns1.google.comforgoogle.com). It only responds to queries regarding the zones it hosts. If a query has theRD(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 theRA(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.
- Authoritative Server: Hosts specific DNS zones (e.g.,
Implement a DNS stub resolver
masterA stub resolver is a DNS client that sends queries to a recursive DNS server. To implement one in Rust:
- Create a
DnsPacket: Initialize a new packet and set therecursion_desiredflag totruein the header. - Add a Question: Push a
DnsQuestion(containing the target domain andQueryType) into the packet's questions vector. - Serialize: Use
packet.write(&mut buffer)to write the packet into aBytePacketBuffer. - Send via UDP: Use
std::net::UdpSocketto send the buffer contents to the target DNS server (e.g.,8.8.8.8:53). - Receive Response: Use
socket.recv_fromto read the response into a buffer, then useDnsPacket::from_bufferto parse the received bytes back into aDnsPacketstructure.
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(()) }- Create a
Implement a DNS Proxy Server in Rust
masterA 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:
- Bind a
UdpSocketto a port (e.g.,2053). - In a loop, use
socket.recv_fromto capture incoming packets and the sender's address (src). - Parse the packet using
DnsPacket::from_buffer. - Extract the question and use a
lookupfunction to forward the query to an upstream server. - Construct a response
DnsPacketwith the sameid, setrecursion_desiredandrecursion_availabletotrue, and setresponsetotrue. - Copy the answers, authorities, and resources from the upstream result into your response packet.
- Handle errors by setting
rescodetoSERVFAIL(if lookup fails) orFORMERR(if no question is present). - Encode the response with
packet.writeand send it back tosrcusingsocket.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); } } }- Bind a
Extend DnsRecord for writing new record types
masterTo implement writing for new record types, extend the
DnsRecord::writemethod. 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 useBytePacketBuffer::set_u16to 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) } }Parse DNS domain names with label compression
masterDNS 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.Inspect DNS packets using dig, netcat, and hexdump
masterYou can manually capture and inspect DNS traffic to understand the protocol structure.
- Listen for a query: Use
nc(netcat) to listen on a specific UDP port and redirect the incoming packet to a file. - Send a query: Use
digwith the+noednsflag (to avoid eDNS extensions and stick to the original 512-byte limit) and point it to your local listener. - Capture a response: Use
ncto send the captured query to a real DNS server (like 8.8.8.8) and redirect the response to a file. - Inspect bytes: Use
hexdump -Cto view the raw hex and ASCII representation of the packets.
- Listen for a query: Use
Extend QueryType with more record types
masterTo support additional DNS record types, update the
QueryTypeenum and implement theto_numandfrom_numutility 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), } } }Extend DnsRecord for reading new record types
masterTo implement reading for new record types, update the
DnsRecordenum to include the new variants and extend theDnsRecord::readmethod. The method should use aBytePacketBufferto parse the preamble (domain, qtype, class, ttl, and data_len) and then match on theQueryTypeto 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
qnamefor the host. - MX: Reads a 2-byte priority followed by a
qnamefor 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) } } }- A: Reads 4 bytes for
Verify DNS Server Behavior with `dig`
masterYou can use the
digutility 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 withraflag). - Test an Authoritative Server (expecting refusal):
dig @ns1.google.com yahoo.com(Should returnstatus: REFUSEDbecauseyahoo.comis not in thegoogle.comzone). - 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 theRDflag).
- Test a Caching Server:
Run DNS server implementation samples
masterThe project provides incremental code samples for each chapter of the guide, named
sample1.rsthroughsample5.rs. You can run these samples usingcargo run --examplefollowed by the sample name.cargo run --example sample1Test recursive lookup with dig
masterTo verify your DNS server is performing recursive lookups correctly, use the
digcommand pointing to your local server's IP and port. For example, if your server is running on127.0.0.1on port2053:dig @127.0.0.1 -p 2053 www.google.comA successful recursive lookup will return an
ANSWER SECTIONcontaining 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