This commit is contained in:
Emil Hernvall
2018-03-15 14:42:42 +01:00
parent 1e6d6ed142
commit 6c5f42cb4b
4 changed files with 115 additions and 192 deletions

View File

@ -216,7 +216,7 @@ impl DnsPacket {
### Implementing a stub resolver ### Implementing a stub resolver
We're ready to implement our stub resolver. Rust includes a convenient 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 ```rust
fn main() { fn main() {
@ -230,7 +230,6 @@ fn main() {
// Bind a UDP socket to an arbitrary port // Bind a UDP socket to an arbitrary port
let socket = UdpSocket::bind(("0.0.0.0", 43210)).unwrap(); let socket = UdpSocket::bind(("0.0.0.0", 43210)).unwrap();
// Build our query packet. It's important that we remember to set the // Build our query packet. It's important that we remember to set the
// `recursion_desired` flag. As noted earlier, the packet id is arbitrary. // `recursion_desired` flag. As noted earlier, the packet id is arbitrary.
let mut packet = DnsPacket::new(); let mut packet = DnsPacket::new();
@ -302,5 +301,3 @@ A {
ttl: 79 ttl: 79
} }
``` ```
We're approaching something useful!

View File

@ -165,13 +165,11 @@ pub fn read(buffer: &mut BytePacketBuffer) -> Result<DnsRecord> {
let _ = try!(buffer.read_u16()); let _ = try!(buffer.read_u16());
let ttl = try!(buffer.read_u32()); let ttl = try!(buffer.read_u32());
let data_len = try!(buffer.read_u16()); 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 { match qtype {
// Handle each record type separately, starting with the A record
// type which remains the same as before.
QueryType::A => { QueryType::A => {
let raw_addr = try!(buffer.read_u32()); let raw_addr = try!(buffer.read_u32());
let addr = Ipv4Addr::new(((raw_addr >> 24) & 0xFF) as u8, let addr = Ipv4Addr::new(((raw_addr >> 24) & 0xFF) as u8,
@ -185,12 +183,9 @@ type which remains the same as before.
ttl: ttl ttl: ttl
}) })
}, },
```
The AAAA record type follows the same logic, but with more numbers to keep // The AAAA record type follows the same logic, but with more numbers to keep
track off. // track off.
```rust
QueryType::AAAA => { QueryType::AAAA => {
let raw_addr1 = try!(buffer.read_u32()); let raw_addr1 = try!(buffer.read_u32());
let raw_addr2 = try!(buffer.read_u32()); let raw_addr2 = try!(buffer.read_u32());
@ -211,11 +206,8 @@ track off.
ttl: ttl ttl: ttl
}) })
}, },
```
NS and CNAME both have the same structure. // NS and CNAME both have the same structure.
```rust
QueryType::NS => { QueryType::NS => {
let mut ns = String::new(); let mut ns = String::new();
try!(buffer.read_qname(&mut ns)); try!(buffer.read_qname(&mut ns));
@ -226,6 +218,7 @@ NS and CNAME both have the same structure.
ttl: ttl ttl: ttl
}) })
}, },
QueryType::CNAME => { QueryType::CNAME => {
let mut cname = String::new(); let mut cname = String::new();
try!(buffer.read_qname(&mut cname)); try!(buffer.read_qname(&mut cname));
@ -236,11 +229,8 @@ NS and CNAME both have the same structure.
ttl: ttl ttl: ttl
}) })
}, },
```
MX is close to the previous two, but with one extra field for priority. // MX is almost like the previous two, but with one extra field for priority.
```rust
QueryType::MX => { QueryType::MX => {
let priority = try!(buffer.read_u16()); let priority = try!(buffer.read_u16());
let mut mx = String::new(); 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 ttl: ttl
}) })
}, },
```
And we end with some code for handling unknown record types, as before. // And we end with some code for handling unknown record types, as before.
```rust
QueryType::UNKNOWN(_) => { QueryType::UNKNOWN(_) => {
try!(buffer.step(data_len as usize)); 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 It's a bit of a mouthful, but there are no especially complicated records in
had. their own right -- it's seeing them all together that makes it look a bit
unwieldy.
### Extending BytePacketBuffer for setting values in place ### Extending BytePacketBuffer for setting values in place
@ -468,5 +456,3 @@ MX {
ttl: 1794 ttl: 1794
} }
``` ```
Encouraging!

View File

@ -2,7 +2,7 @@
=========================== ===========================
Haven gotten this far, we're ready to make our first attempt at writing an 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 * Authoritative Server - A DNS server hosting one or more "zones". For
instance, the authoritative servers for the zone google.com are 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. 8.8.8.8 and 8.8.4.4.
Strictly speaking, there's nothing to stop a server from doing both things, but 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 the significance of the flags `RD` (Recursion Desired) and `RA` (Recursion
Available) in the packet header -- a stub resolver querying a caching server 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 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 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: response. We can still use the same server for *google.com*, however:
```text ```text
@ -177,19 +177,13 @@ fn main() {
// Bind an UDP socket on port 2053 // Bind an UDP socket on port 2053
let socket = UdpSocket::bind(("0.0.0.0", 2053)).unwrap(); let socket = UdpSocket::bind(("0.0.0.0", 2053)).unwrap();
```
For now, queries are handled sequentially, so an infinite loop for servicing // For now, queries are handled sequentially, so an infinite loop for servicing
requests is initiated. // requests is initiated.
```rust
loop { loop {
```
With a socket ready, we can go ahead and read a packet. This will block until // With a socket ready, we can go ahead and read a packet. This will
one is received. // block until one is received.
```rust
let mut req_buffer = BytePacketBuffer::new(); let mut req_buffer = BytePacketBuffer::new();
let (_, src) = match socket.recv_from(&mut req_buffer.buf) { let (_, src) = match socket.recv_from(&mut req_buffer.buf) {
Ok(x) => x, Ok(x) => x,
@ -198,19 +192,17 @@ one is received.
continue; continue;
} }
}; };
```
Here we use match to safely unwrap the `Result`. If everything's as expected, // 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 // 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 // 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 // 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 // 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. // track of the source in order to send our reply later on.
Next, `DnsPacket::from_buffer` is used to parse the raw bytes into // 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. // a `DnsPacket`. It uses the same error handling idiom as the previous statement.
```rust
let request = match DnsPacket::from_buffer(&mut req_buffer) { let request = match DnsPacket::from_buffer(&mut req_buffer) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
@ -218,43 +210,31 @@ a `DnsPacket`. It uses the same error handling idiom as the previous statement.
continue; continue;
} }
}; };
```
At this stage, the response packet is created and initiated. // Create and initialize the response packet
```rust
let mut packet = DnsPacket::new(); let mut packet = DnsPacket::new();
packet.header.id = request.header.id; packet.header.id = request.header.id;
packet.header.recursion_desired = true; packet.header.recursion_desired = true;
packet.header.recursion_available = true; packet.header.recursion_available = true;
packet.header.response = true; packet.header.response = true;
```
Being mindful of how unreliable input data from arbitrary senders can be, we // 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` // need make sure that a question is actually present. If not, we return `FORMERR`
to indicate that the sender made something wrong. // to indicate that the sender made something wrong.
```rust
if request.questions.is_empty() { if request.questions.is_empty() {
packet.header.rescode = ResultCode::FORMERR; packet.header.rescode = ResultCode::FORMERR;
} }
```
Usually a question will be present, though. // Usually a question will be present, though.
```rust
else { else {
let question = &request.questions[0]; let question = &request.questions[0];
println!("Received query: {:?}", question); println!("Received query: {:?}", question);
```
Since all is set up and as expected, the query can be forwarded to the target // 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 // 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 // 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 // rather everything goes as planned, the question and response records as copied
into our response packet. // into our response packet.
```rust
if let Ok(result) = lookup(&question.name, question.qtype, server) { if let Ok(result) = lookup(&question.name, question.qtype, server) {
packet.questions.push(question.clone()); packet.questions.push(question.clone());
packet.header.rescode = result.header.rescode; packet.header.rescode = result.header.rescode;
@ -274,11 +254,9 @@ into our response packet.
} else { } else {
packet.header.rescode = ResultCode::SERVFAIL; 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(); let mut res_buffer = BytePacketBuffer::new();
match packet.write(&mut res_buffer) { match packet.write(&mut res_buffer) {
Ok(_) => {}, Ok(_) => {},
@ -304,18 +282,16 @@ The only thing remaining is to encode our response and send it off!
continue; 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
} // End of request loop } // End of request loop
} // End of main } // 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 All done! Let's try it! We start our server in one terminal, and use `dig` to
perform a lookup in a second terminal. 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 } 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! queries with several different record types!

View File

@ -167,14 +167,10 @@ Before we can get on, we'll need a few utility functions on `DnsPacket`.
impl DnsPacket { impl DnsPacket {
- snip - - snip -
```
First, it's useful to be able to pick a random A record from a packet. Since we // It's useful to be able to pick a random A record from a packet. When we
don't want to introduce an external dependency, and there's no method for // get multiple IP's for a single name, it doesn't matter which one we
generating random numbers in the rust standard library, we'll just pick the // choose, so in those cases we can now pick one at random.
first entry for now.
```rust
pub fn get_random_a(&self) -> Option<String> { pub fn get_random_a(&self) -> Option<String> {
if !self.answers.is_empty() { if !self.answers.is_empty() {
let idx = random::<usize>() % self.answers.len(); let idx = random::<usize>() % self.answers.len();
@ -186,31 +182,22 @@ first entry for now.
None None
} }
```
Second, we'll use the fact that name servers often bundle the corresponding // 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 // A records when replying to an NS query to implement a function that returns
the actual IP for an NS record if possible. // the actual IP for an NS record if possible.
```rust
pub fn get_resolved_ns(&self, qname: &str) -> Option<String> { pub fn get_resolved_ns(&self, qname: &str) -> Option<String> {
```
First, we scan the list of NS records in the authorities section: // First, we scan the list of NS records in the authorities section:
```rust
let mut new_authorities = Vec::new(); let mut new_authorities = Vec::new();
for auth in &self.authorities { for auth in &self.authorities {
if let DnsRecord::NS { ref domain, ref host, .. } = *auth { if let DnsRecord::NS { ref domain, ref host, .. } = *auth {
if !qname.ends_with(domain) { if !qname.ends_with(domain) {
continue; continue;
} }
```
Once we've found an NS record, we scan the resources record for a matching // Once we've found an NS record, we scan the resources record for a matching
A record... // A record...
```rust
for rsrc in &self.resources { for rsrc in &self.resources {
if let DnsRecord::A{ ref domain, ref addr, ttl } = *rsrc { if let DnsRecord::A{ ref domain, ref addr, ttl } = *rsrc {
if domain != host { if domain != host {
@ -222,22 +209,15 @@ A record...
addr: *addr, addr: *addr,
ttl: ttl ttl: ttl
}; };
```
...and push any matches to a list. // ...and push any matches to a list.
```rust
new_authorities.push(rec); new_authorities.push(rec);
} }
} }
} }
} }
```
If there are any matches, we pick the first one. Again, we'll want to introduce // If there are any matches, we pick the first one.
randomization later on.
```rust
if !new_authorities.is_empty() { if !new_authorities.is_empty() {
if let DnsRecord::A { addr, .. } = new_authorities[0] { if let DnsRecord::A { addr, .. } = new_authorities[0] {
return Some(addr.to_string()); return Some(addr.to_string());
@ -246,14 +226,11 @@ randomization later on.
None None
} // End of get_resolved_ns } // End of get_resolved_ns
```
However, not all name servers are as well behaved. In certain cases there won't // 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* // 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 // lookup in the midst. For this, we introduce a method for returning the host
name of an appropriate name server. // name of an appropriate name server.
```rust
pub fn get_unresolved_ns(&self, qname: &str) -> Option<String> { pub fn get_unresolved_ns(&self, qname: &str) -> Option<String> {
let mut new_authorities = Vec::new(); let mut new_authorities = Vec::new();
@ -284,83 +261,56 @@ We move swiftly on to our new `recursive_lookup` function:
```rust ```rust
fn recursive_lookup(qname: &str, qtype: QueryType) -> Result<DnsPacket> { fn recursive_lookup(qname: &str, qtype: QueryType) -> Result<DnsPacket> {
```
For now we're always starting with *a.root-servers.net*. // For now we're always starting with *a.root-servers.net*.
```rust
let mut ns = "198.41.0.4".to_string(); let mut ns = "198.41.0.4".to_string();
```
Since it might take an arbitrary number of steps, we enter an unbounded loop. // Since it might take an arbitrary number of steps, we enter an unbounded loop.
```rust
loop { loop {
println!("attempting lookup of {:?} {} with ns {}", qtype, qname, ns); println!("attempting lookup of {:?} {} with ns {}", qtype, qname, ns);
```
The next step is to send the query to the active server. // The next step is to send the query to the active server.
```rust
let ns_copy = ns.clone(); let ns_copy = ns.clone();
let server = (ns_copy.as_str(), 53); let server = (ns_copy.as_str(), 53);
let response = try!(lookup(qname, qtype.clone(), server)); let response = try!(lookup(qname, qtype.clone(), server));
```
If there are entries in the answer section, and no errors, we are done! // If there are entries in the answer section, and no errors, we are done!
```rust
if !response.answers.is_empty() && if !response.answers.is_empty() &&
response.header.rescode == ResultCode::NOERROR { response.header.rescode == ResultCode::NOERROR {
return Ok(response.clone()); return Ok(response.clone());
} }
```
We might also get a `NXDOMAIN` reply, which is the authoritative name servers // We might also get a `NXDOMAIN` reply, which is the authoritative name servers
way of telling us that the name doesn't exist. // way of telling us that the name doesn't exist.
```rust
if response.header.rescode == ResultCode::NXDOMAIN { if response.header.rescode == ResultCode::NXDOMAIN {
return Ok(response.clone()); return Ok(response.clone());
} }
```
Otherwise, we'll try to find a new nameserver based on NS and a corresponding A // 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 // record in the additional section. If this succeeds, we can switch name server
and retry the loop. // and retry the loop.
```rust
if let Some(new_ns) = response.get_resolved_ns(qname) { if let Some(new_ns) = response.get_resolved_ns(qname) {
ns = new_ns.clone(); ns = new_ns.clone();
continue; continue;
} }
```
If not, we'll have to resolve the ip of a NS record. If no NS records exist, // 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. // we'll go with what the last server told us.
```rust
let new_ns_name = match response.get_unresolved_ns(qname) { let new_ns_name = match response.get_unresolved_ns(qname) {
Some(x) => x, Some(x) => x,
None => return Ok(response.clone()) None => return Ok(response.clone())
}; };
```
Here we go down the rabbit hole by starting _another_ lookup sequence in the // 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 // midst of our current one. Hopefully, this will give us the IP of an appropriate
name server. // name server.
```rust
let recursive_response = try!(recursive_lookup(&new_ns_name, QueryType::A)); 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 // 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. // record is available, we again return the last result we got.
```rust
if let Some(new_ns) = recursive_response.get_random_a() { if let Some(new_ns) = recursive_response.get_random_a() {
ns = new_ns.clone(); ns = new_ns.clone();
} else { } 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 } 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.