From 6c5f42cb4bdca9dd88b646474bcb8a33f5791904 Mon Sep 17 00:00:00 2001 From: Emil Hernvall Date: Thu, 15 Mar 2018 14:42:42 +0100 Subject: [PATCH] Tweaks --- chapter2.md | 5 +- chapter3.md | 38 +++++---------- chapter4.md | 136 ++++++++++++++++++++++------------------------------ chapter5.md | 128 ++++++++++++++++++------------------------------- 4 files changed, 115 insertions(+), 192 deletions(-) diff --git a/chapter2.md b/chapter2.md index f782896..2de32d3 100644 --- a/chapter2.md +++ b/chapter2.md @@ -216,7 +216,7 @@ impl DnsPacket { ### Implementing a stub resolver We're ready to implement our stub resolver. Rust includes a convenient -`UDPSocket` which does most of the work. First there's some house keeping: +`UDPSocket` which does most of the work. ```rust fn main() { @@ -230,7 +230,6 @@ fn main() { // Bind a UDP socket to an arbitrary port let socket = UdpSocket::bind(("0.0.0.0", 43210)).unwrap(); - // Build our query packet. It's important that we remember to set the // `recursion_desired` flag. As noted earlier, the packet id is arbitrary. let mut packet = DnsPacket::new(); @@ -302,5 +301,3 @@ A { ttl: 79 } ``` - -We're approaching something useful! diff --git a/chapter3.md b/chapter3.md index 76e5e6f..9008b08 100644 --- a/chapter3.md +++ b/chapter3.md @@ -165,13 +165,11 @@ pub fn read(buffer: &mut BytePacketBuffer) -> Result { let _ = try!(buffer.read_u16()); let ttl = try!(buffer.read_u32()); let data_len = try!(buffer.read_u16()); -``` -After which we handle each record type separately, starting with the A record -type which remains the same as before. - -```rust match qtype { + + // Handle each record type separately, starting with the A record + // type which remains the same as before. QueryType::A => { let raw_addr = try!(buffer.read_u32()); let addr = Ipv4Addr::new(((raw_addr >> 24) & 0xFF) as u8, @@ -185,12 +183,9 @@ type which remains the same as before. ttl: ttl }) }, -``` -The AAAA record type follows the same logic, but with more numbers to keep -track off. - -```rust + // The AAAA record type follows the same logic, but with more numbers to keep + // track off. QueryType::AAAA => { let raw_addr1 = try!(buffer.read_u32()); let raw_addr2 = try!(buffer.read_u32()); @@ -211,11 +206,8 @@ track off. ttl: ttl }) }, -``` -NS and CNAME both have the same structure. - -```rust + // NS and CNAME both have the same structure. QueryType::NS => { let mut ns = String::new(); try!(buffer.read_qname(&mut ns)); @@ -226,6 +218,7 @@ NS and CNAME both have the same structure. ttl: ttl }) }, + QueryType::CNAME => { let mut cname = String::new(); try!(buffer.read_qname(&mut cname)); @@ -236,11 +229,8 @@ NS and CNAME both have the same structure. ttl: ttl }) }, -``` -MX is close to the previous two, but with one extra field for priority. - -```rust + // MX is almost like the previous two, but with one extra field for priority. QueryType::MX => { let priority = try!(buffer.read_u16()); let mut mx = String::new(); @@ -253,11 +243,8 @@ MX is close to the previous two, but with one extra field for priority. ttl: ttl }) }, -``` -And we end with some code for handling unknown record types, as before. - -```rust + // And we end with some code for handling unknown record types, as before. QueryType::UNKNOWN(_) => { try!(buffer.step(data_len as usize)); @@ -272,8 +259,9 @@ And we end with some code for handling unknown record types, as before. } ``` -It's a bit of a mouthful, but individually not much more complex than what we -had. +It's a bit of a mouthful, but there are no especially complicated records in +their own right -- it's seeing them all together that makes it look a bit +unwieldy. ### Extending BytePacketBuffer for setting values in place @@ -468,5 +456,3 @@ MX { ttl: 1794 } ``` - -Encouraging! diff --git a/chapter4.md b/chapter4.md index 437d00b..e378c08 100644 --- a/chapter4.md +++ b/chapter4.md @@ -2,7 +2,7 @@ =========================== Haven gotten this far, we're ready to make our first attempt at writing an -actual server. In reality, DNS servers fullfil two different purposes: +actual server. Real DNS servers come in two different varieties: * Authoritative Server - A DNS server hosting one or more "zones". For instance, the authoritative servers for the zone google.com are @@ -15,7 +15,7 @@ actual server. In reality, DNS servers fullfil two different purposes: 8.8.8.8 and 8.8.4.4. Strictly speaking, there's nothing to stop a server from doing both things, but -in pracice these two roles are typically mutually exclusive. This also explains +in practice these two roles are typically mutually exclusive. This also explains the significance of the flags `RD` (Recursion Desired) and `RA` (Recursion Available) in the packet header -- a stub resolver querying a caching server will set the `RD` flag, and since the server allows such queries it will @@ -77,7 +77,7 @@ servers hosting the *google.com* zone: ``` Notice how the status of the response says `REFUSED`! `dig` also warns us that -while the `RD` flag was set in the query, the server didn't set it in the +while the `RD` flag was set in the query, the server didn't set the `RA` flag in the response. We can still use the same server for *google.com*, however: ```text @@ -177,19 +177,13 @@ fn main() { // Bind an UDP socket on port 2053 let socket = UdpSocket::bind(("0.0.0.0", 2053)).unwrap(); -``` -For now, queries are handled sequentially, so an infinite loop for servicing -requests is initiated. - -```rust + // For now, queries are handled sequentially, so an infinite loop for servicing + // requests is initiated. loop { -``` -With a socket ready, we can go ahead and read a packet. This will block until -one is received. - -```rust + // With a socket ready, we can go ahead and read a packet. This will + // block until one is received. let mut req_buffer = BytePacketBuffer::new(); let (_, src) = match socket.recv_from(&mut req_buffer.buf) { Ok(x) => x, @@ -198,19 +192,17 @@ one is received. continue; } }; -``` -Here we use match to safely unwrap the `Result`. If everything's as expected, -the raw bytes are simply returned, and if not it'll abort by restarting the -loop and waiting for the next request. The `recv_from` function will write the -data into the provided buffer, and return the length of the data read as well -as the source adress. We're not interested in the length, but we need to keep -track of the source in order to send our reply later on. + // Here we use match to safely unwrap the `Result`. If everything's as expected, + // the raw bytes are simply returned, and if not it'll abort by restarting the + // loop and waiting for the next request. The `recv_from` function will write the + // data into the provided buffer, and return the length of the data read as well + // as the source adress. We're not interested in the length, but we need to keep + // track of the source in order to send our reply later on. -Next, `DnsPacket::from_buffer` is used to parse the raw bytes into -a `DnsPacket`. It uses the same error handling idiom as the previous statement. + // Next, `DnsPacket::from_buffer` is used to parse the raw bytes into + // a `DnsPacket`. It uses the same error handling idiom as the previous statement. -```rust let request = match DnsPacket::from_buffer(&mut req_buffer) { Ok(x) => x, Err(e) => { @@ -218,43 +210,31 @@ a `DnsPacket`. It uses the same error handling idiom as the previous statement. continue; } }; -``` -At this stage, the response packet is created and initiated. - -```rust + // Create and initialize the response packet 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; -``` -Being mindful of how unreliable input data from arbitrary senders can be, we -need make sure that a question is actually present. If not, we return `FORMERR` -to indicate that the sender made something wrong. - -```rust + // Being mindful of how unreliable input data from arbitrary senders can be, we + // need make sure that a question is actually present. If not, we return `FORMERR` + // to indicate that the sender made something wrong. if request.questions.is_empty() { packet.header.rescode = ResultCode::FORMERR; } -``` -Usually a question will be present, though. - -```rust + // Usually a question will be present, though. else { let question = &request.questions[0]; println!("Received query: {:?}", question); -``` -Since all is set up and as expected, the query can be forwarded to the target -server. There's always the possibility that the query will fail, in which case -the `SERVFAIL` response code is set to indicate as much to the client. If -rather everything goes as planned, the question and response records as copied -into our response packet. - -```rust + // Since all is set up and as expected, the query can be forwarded to the target + // server. There's always the possibility that the query will fail, in which case + // the `SERVFAIL` response code is set to indicate as much to the client. If + // rather everything goes as planned, the question and response records as copied + // into our response packet. if let Ok(result) = lookup(&question.name, question.qtype, server) { packet.questions.push(question.clone()); packet.header.rescode = result.header.rescode; @@ -274,48 +254,44 @@ into our response packet. } else { packet.header.rescode = ResultCode::SERVFAIL; } -``` -The only thing remaining is to encode our response and send it off! + // The only thing remaining is to encode our response and send it off! -```rust - let mut res_buffer = BytePacketBuffer::new(); - match packet.write(&mut res_buffer) { - Ok(_) => {}, - Err(e) => { - println!("Failed to encode UDP response packet: {:?}", e); - continue; - } - }; + let mut res_buffer = BytePacketBuffer::new(); + match packet.write(&mut res_buffer) { + Ok(_) => {}, + Err(e) => { + println!("Failed to encode UDP response packet: {:?}", e); + continue; + } + }; - let len = res_buffer.pos(); - let data = match res_buffer.get_range(0, len) { - Ok(x) => x, - Err(e) => { - println!("Failed to retrieve response buffer: {:?}", e); - continue; - } - }; + let len = res_buffer.pos(); + let data = match res_buffer.get_range(0, len) { + Ok(x) => x, + Err(e) => { + println!("Failed to retrieve response buffer: {:?}", e); + continue; + } + }; - match socket.send_to(data, src) { - Ok(_) => {}, - Err(e) => { - println!("Failed to send response buffer: {:?}", e); - continue; - } - }; -``` - -The match idiom for error handling is used again here, since we want to avoid -terminating our request loop at all cost. It's a bit verbose, and normally we'd -like to use `try!` instead. Unfortunately that's unavailable to us here, since -we're in the `main` function which doesn't return a `Result`. - -```rust + match socket.send_to(data, src) { + Ok(_) => {}, + Err(e) => { + println!("Failed to send response buffer: {:?}", e); + continue; + } + }; + } } // End of request loop } // End of main ``` +The match idiom for error handling is used again and again here, since we want to avoid +terminating our request loop at all cost. It's a bit verbose, and normally we'd +like to use `try!` instead. Unfortunately that's unavailable to us here, since +we're in the `main` function which doesn't return a `Result`. + All done! Let's try it! We start our server in one terminal, and use `dig` to perform a lookup in a second terminal. @@ -348,5 +324,5 @@ Received query: DnsQuestion { name: "google.com", qtype: A } Answer: A { domain: "google.com", addr: 216.58.211.142, ttl: 96 } ``` -In less than 800 lines of code, we've built a DNS server able to respond to +Success! In less than 800 lines of code, we've built a DNS server able to respond to queries with several different record types! diff --git a/chapter5.md b/chapter5.md index d9b52f7..0fb6650 100644 --- a/chapter5.md +++ b/chapter5.md @@ -167,14 +167,10 @@ Before we can get on, we'll need a few utility functions on `DnsPacket`. impl DnsPacket { - snip - -``` -First, it's useful to be able to pick a random A record from a packet. Since we -don't want to introduce an external dependency, and there's no method for -generating random numbers in the rust standard library, we'll just pick the -first entry for now. - -```rust + // It's useful to be able to pick a random A record from a packet. When we + // get multiple IP's for a single name, it doesn't matter which one we + // choose, so in those cases we can now pick one at random. pub fn get_random_a(&self) -> Option { if !self.answers.is_empty() { let idx = random::() % self.answers.len(); @@ -186,31 +182,22 @@ first entry for now. None } -``` -Second, we'll use the fact that name servers often bundle the corresponding -A records when replying to an NS query to implement a function that returns -the actual IP for an NS record if possible. - -```rust + // We'll use the fact that name servers often bundle the corresponding + // A records when replying to an NS query to implement a function that returns + // the actual IP for an NS record if possible. pub fn get_resolved_ns(&self, qname: &str) -> Option { -``` -First, we scan the list of NS records in the authorities section: - -```rust + // First, we scan the list of NS records in the authorities section: let mut new_authorities = Vec::new(); for auth in &self.authorities { if let DnsRecord::NS { ref domain, ref host, .. } = *auth { if !qname.ends_with(domain) { continue; } -``` -Once we've found an NS record, we scan the resources record for a matching -A record... - -```rust + // Once we've found an NS record, we scan the resources record for a matching + // A record... for rsrc in &self.resources { if let DnsRecord::A{ ref domain, ref addr, ttl } = *rsrc { if domain != host { @@ -222,22 +209,15 @@ A record... addr: *addr, ttl: ttl }; -``` -...and push any matches to a list. - -```rust + // ...and push any matches to a list. new_authorities.push(rec); } } } } -``` -If there are any matches, we pick the first one. Again, we'll want to introduce -randomization later on. - -```rust + // If there are any matches, we pick the first one. if !new_authorities.is_empty() { if let DnsRecord::A { addr, .. } = new_authorities[0] { return Some(addr.to_string()); @@ -246,14 +226,11 @@ randomization later on. None } // End of get_resolved_ns -``` -However, not all name servers are as well behaved. In certain cases there won't -be any A records in the additional section, and we'll have to perform *another* -lookup in the midst. For this, we introduce a method for returning the host -name of an appropriate name server. - -```rust + // However, not all name servers are as that nice. In certain cases there won't + // be any A records in the additional section, and we'll have to perform *another* + // lookup in the midst. For this, we introduce a method for returning the host + // name of an appropriate name server. pub fn get_unresolved_ns(&self, qname: &str) -> Option { let mut new_authorities = Vec::new(); @@ -284,83 +261,56 @@ We move swiftly on to our new `recursive_lookup` function: ```rust fn recursive_lookup(qname: &str, qtype: QueryType) -> Result { -``` -For now we're always starting with *a.root-servers.net*. - -```rust + // For now we're always starting with *a.root-servers.net*. let mut ns = "198.41.0.4".to_string(); -``` -Since it might take an arbitrary number of steps, we enter an unbounded loop. - -```rust + // Since it might take an arbitrary number of steps, we enter an unbounded loop. loop { println!("attempting lookup of {:?} {} with ns {}", qtype, qname, ns); -``` -The next step is to send the query to the active server. - -```rust + // The next step is to send the query to the active server. let ns_copy = ns.clone(); let server = (ns_copy.as_str(), 53); let response = try!(lookup(qname, qtype.clone(), server)); -``` -If there are entries in the answer section, and no errors, we are done! - -```rust + // If there are entries in the answer section, and no errors, we are done! if !response.answers.is_empty() && response.header.rescode == ResultCode::NOERROR { return Ok(response.clone()); } -``` -We might also get a `NXDOMAIN` reply, which is the authoritative name servers -way of telling us that the name doesn't exist. - -```rust + // We might also get a `NXDOMAIN` reply, which is the authoritative name servers + // way of telling us that the name doesn't exist. if response.header.rescode == ResultCode::NXDOMAIN { return Ok(response.clone()); } -``` -Otherwise, we'll try to find a new nameserver based on NS and a corresponding A -record in the additional section. If this succeeds, we can switch name server -and retry the loop. - -```rust + // Otherwise, we'll try to find a new nameserver based on NS and a corresponding A + // record in the additional section. If this succeeds, we can switch name server + // and retry the loop. if let Some(new_ns) = response.get_resolved_ns(qname) { ns = new_ns.clone(); continue; } -``` -If not, we'll have to resolve the ip of a NS record. If no NS records exist, -we'll go with what the last server told us. - -```rust + // If not, we'll have to resolve the ip of a NS record. If no NS records exist, + // we'll go with what the last server told us. let new_ns_name = match response.get_unresolved_ns(qname) { Some(x) => x, None => return Ok(response.clone()) }; -``` -Here we go down the rabbit hole by starting _another_ lookup sequence in the -midst of our current one. Hopefully, this will give us the IP of an appropriate -name server. - -```rust + // Here we go down the rabbit hole by starting _another_ lookup sequence in the + // midst of our current one. Hopefully, this will give us the IP of an appropriate + // name server. let recursive_response = try!(recursive_lookup(&new_ns_name, QueryType::A)); -``` -Finally, we pick a random ip from the result, and restart the loop. If no such -record is available, we again return the last result we got. - -```rust + // Finally, we pick a random ip from the result, and restart the loop. If no such + // record is available, we again return the last result we got. if let Some(new_ns) = recursive_response.get_random_a() { ns = new_ns.clone(); } else { @@ -424,4 +374,18 @@ attempting lookup of A www.google.com with ns 216.239.34.10 Answer: A { domain: "www.google.com", addr: 216.58.211.132, ttl: 300 } ``` -This mirrors our manual process earlier. We're really getting somewhere! +This mirrors our manual process earlier. We can now successfully resolve +a domain starting from the list of root servers. We've now got a fully +functional, albeit suboptimal, DNS server. + +There are many things that we could do better. For instance, there is no true +concurrency in this server. We can neither send nor receive queries over TCP. +We cannot use it to host our own zones, and allow it to act as an authorative +server. The lack of support for DNSSEC leaves us open to DNS poisoning attacks +where a malicious server can return records relating to somebody else's domain. + +Many of these problems have been fixed in my own project +[hermes](https://github.com/EmilHernvall/hermes), so you can head over there to +investigate how I did it, or continue on your own from here. Or maybe you've +had enough of DNS for now... :) Regardless, I hope you've gained some new insight +into how DNS works.