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
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!

View File

@ -165,13 +165,11 @@ pub fn read(buffer: &mut BytePacketBuffer) -> Result<DnsRecord> {
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!

View File

@ -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!

View File

@ -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<String> {
if !self.answers.is_empty() {
let idx = random::<usize>() % 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<String> {
```
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<String> {
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<DnsPacket> {
```
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.