dnsguide/chapter4.md

307 lines
10 KiB
Markdown
Raw Normal View History

2017-11-23 20:52:49 +07:00
4 - Baby's first DNS server
===========================
2017-11-23 20:51:20 +07:00
Haven gotten this far, we're ready to make our first attempt at writing an
2018-03-15 20:42:42 +07:00
actual server. Real DNS servers come in two different varieties:
2017-11-23 20:51:20 +07:00
* Authoritative Server - A DNS server hosting one or more "zones". For
instance, the authoritative servers for the zone google.com are
ns1.google.com, ns2.google.com, ns3.google.com and ns4.google.com.
* Caching Server - A DNS server that services DNS lookups by first checking
its cache to see if it already knows of the record being requested, and if
not performing a recursive lookup to figure it out. This includes the DNS
server that is likely running on your home router as well as the DNS server
that your ISP assigns to you through DHCP, and Google's public DNS servers
8.8.8.8 and 8.8.4.4.
Strictly speaking, there's nothing to stop a server from doing both things, but
2018-03-15 20:42:42 +07:00
in practice these two roles are typically mutually exclusive. This also explains
2017-11-23 20:51:20 +07:00
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
perform the lookup and send a reply with the `RA` flag set. This won't work for
an Authoritative Server which will only reply to queries relating to the zones
hosted, and as such will send an error response to any queries with the `RD`
flag set.
Don't take my word for it, though! Let's verify that this is the case. First
off, let's use `8.8.8.8` for looking up *yahoo.com*:
```text
# dig @8.8.8.8 yahoo.com
; <<>> DiG 9.10.3-P4-Ubuntu <<>> +recurse @8.8.8.8 yahoo.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53231
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;yahoo.com. IN A
;; ANSWER SECTION:
yahoo.com. 1051 IN A 98.138.253.109
yahoo.com. 1051 IN A 98.139.183.24
yahoo.com. 1051 IN A 206.190.36.45
;; Query time: 1 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Jul 08 11:43:55 CEST 2016
;; MSG SIZE rcvd: 86
```
This works as expected. Now let's try sending the same query to one of the
servers hosting the *google.com* zone:
```text
# dig @ns1.google.com yahoo.com
; <<>> DiG 9.10.3-P4-Ubuntu <<>> +recurse @ns1.google.com yahoo.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 12034
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;yahoo.com. IN A
;; Query time: 10 msec
;; SERVER: 216.239.32.10#53(216.239.32.10)
;; WHEN: Fri Jul 08 11:44:07 CEST 2016
;; MSG SIZE rcvd: 27
```
Notice how the status of the response says `REFUSED`! `dig` also warns us that
2018-03-15 20:42:42 +07:00
while the `RD` flag was set in the query, the server didn't set the `RA` flag in the
2017-11-23 20:51:20 +07:00
response. We can still use the same server for *google.com*, however:
```text
dig @ns1.google.com google.com <<<
; <<>> DiG 9.10.3-P4-Ubuntu <<>> +recurse @ns1.google.com google.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28058
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 300 IN A 216.58.211.142
;; Query time: 10 msec
;; SERVER: 216.239.32.10#53(216.239.32.10)
;; WHEN: Fri Jul 08 11:46:27 CEST 2016
;; MSG SIZE rcvd: 44
```
No error this time -- however, `dig` still warns us that recursion is
unavailable. We can explicitly unset it using `+norecurse` which gets rid of
the warning:
```text
# dig +norecurse @ns1.google.com google.com
; <<>> DiG 9.10.3-P4-Ubuntu <<>> +norecurse @ns1.google.com google.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15850
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 300 IN A 216.58.211.142
;; Query time: 10 msec
;; SERVER: 216.239.32.10#53(216.239.32.10)
;; WHEN: Fri Jul 08 11:47:52 CEST 2016
;; MSG SIZE rcvd: 44
```
This final query is the type of query that we'd expect to see a caching server
send as part of recursively resolving the name.
For our first foray into writing our own server, we'll do something even
simpler by implementing a server that simply forwards queries to another
caching server, i.e. a "DNS proxy server". Having already done most of the hard
work, it's a rather quick effort!
### Separating lookup into a separate function
We'll start out by doing some quick refactoring, moving our lookup code into
a separate function. This is for the most part the same code as we had in our
2020-06-18 06:47:09 +07:00
`main` function in the previous chapter.
2017-11-23 20:51:20 +07:00
```rust
fn lookup(qname: &str, qtype: QueryType) -> Result<DnsPacket> {
// Forward queries to Google's public DNS
let server = ("8.8.8.8", 53);
2020-06-18 06:47:09 +07:00
let socket = UdpSocket::bind(("0.0.0.0", 43210))?;
2017-11-23 20:51:20 +07:00
let mut packet = DnsPacket::new();
packet.header.id = 6666;
packet.header.questions = 1;
packet.header.recursion_desired = true;
2020-06-18 06:47:09 +07:00
packet
.questions
.push(DnsQuestion::new(qname.to_string(), qtype));
2017-11-23 20:51:20 +07:00
let mut req_buffer = BytePacketBuffer::new();
2020-06-18 06:47:09 +07:00
packet.write(&mut req_buffer)?;
socket.send_to(&req_buffer.buf[0..req_buffer.pos], server)?;
2017-11-23 20:51:20 +07:00
let mut res_buffer = BytePacketBuffer::new();
2020-06-18 06:47:09 +07:00
socket.recv_from(&mut res_buffer.buf)?;
2017-11-23 20:51:20 +07:00
DnsPacket::from_buffer(&mut res_buffer)
}
2017-11-23 20:51:20 +07:00
```
### Implementing our first server
Now we'll write our server code. First, we need get some things in order.
```rust
/// Handle a single incoming packet
fn handle_query(socket: &UdpSocket) -> Result<()> {
// 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();
2017-11-23 20:51:20 +07:00
// 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 address.
// We're not interested in the length, but we need to keep track of the
// source in order to send our reply later on.
let (_, src) = socket.recv_from(&mut req_buffer.buf)?;
2017-11-23 20:51:20 +07:00
// Next, `DnsPacket::from_buffer` is used to parse the raw bytes into
// a `DnsPacket`.
2020-06-18 07:23:42 +07:00
let mut request = DnsPacket::from_buffer(&mut req_buffer)?;
2017-11-23 20:51:20 +07:00
// 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;
2020-06-18 07:23:42 +07:00
// In the normal case, exactly one question is present
if let Some(question) = request.questions.pop() {
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.
if let Ok(result) = lookup(&question.name, question.qtype) {
2020-06-18 07:23:42 +07:00
packet.questions.push(question);
packet.header.rescode = result.header.rescode;
for rec in result.answers {
println!("Answer: {:?}", rec);
packet.answers.push(rec);
2017-11-23 20:51:20 +07:00
}
for rec in result.authorities {
println!("Authority: {:?}", rec);
packet.authorities.push(rec);
2017-11-23 20:51:20 +07:00
}
for rec in result.resources {
println!("Resource: {:?}", rec);
packet.resources.push(rec);
2017-11-23 20:51:20 +07:00
}
} else {
packet.header.rescode = ResultCode::SERVFAIL;
2020-06-18 06:47:09 +07:00
}
}
2020-06-18 07:23:42 +07:00
// 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.
else {
packet.header.rescode = ResultCode::FORMERR;
}
2017-11-23 20:51:20 +07:00
// The only thing remaining is to encode our response and send it off!
let mut res_buffer = BytePacketBuffer::new();
packet.write(&mut res_buffer)?;
2017-11-23 20:51:20 +07:00
let len = res_buffer.pos();
let data = res_buffer.get_range(0, len)?;
socket.send_to(data, src)?;
Ok(())
}
fn main() -> Result<()> {
// Bind an UDP socket on port 2053
let socket = UdpSocket::bind(("0.0.0.0", 2053))?;
// For now, queries are handled sequentially, so an infinite loop for servicing
// requests is initiated.
loop {
match handle_query(&socket) {
Ok(_) => {},
Err(e) => eprintln!("An error occured: {}", e),
}
2020-06-18 06:47:09 +07:00
}
}
2017-11-23 20:51:20 +07:00
```
2018-03-15 20:42:42 +07:00
The match idiom for error handling is used again and again here, since we want to avoid
2017-11-23 20:51:20 +07:00
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.
```text
# dig @127.0.0.1 -p 2053 google.com
; <<>> DiG 9.10.3-P4-Ubuntu <<>> @127.0.0.1 -p 2053 google.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47200
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 68 IN A 216.58.211.142
;; Query time: 1 msec
;; SERVER: 127.0.0.1#2053(127.0.0.1)
;; WHEN: Fri Jul 08 12:07:44 CEST 2016
;; MSG SIZE rcvd: 54
```
Looking at our server terminal we see:
```text
Received query: DnsQuestion { name: "google.com", qtype: A }
Answer: A { domain: "google.com", addr: 216.58.211.142, ttl: 96 }
```
2018-03-15 20:42:42 +07:00
Success! In less than 800 lines of code, we've built a DNS server able to respond to
2017-11-23 20:51:20 +07:00
queries with several different record types!
2018-03-19 17:33:11 +07:00
In the next chapter, we'll get rid of our dependence on an existing resolver:
[Chapter 5 - Recursive Resolve](/chapter5.md)