mirror of
https://github.com/EmilHernvall/dnsguide.git
synced 2024-12-22 20:04:16 +07:00
Second chapter
This commit is contained in:
parent
9706e94956
commit
700334b7ad
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
||||
Building a DNS server in Rust
|
||||
=============================
|
||||
|
||||
To me, DNS is one the most exciting aspects of the Internet. Before it was
|
||||
invented, everyone on the internet - which admittedly wasn't that many at that
|
||||
stage - relied on a shared file called HOSTS.TXT, maintained by the Stanford
|
||||
Research Institute. This file was synchronized manually through FTP, and as the
|
||||
number of hosts grew, so did the rate of change and the unfeasibility of the
|
||||
system. In 1983, Paul Mockapetris set out to find a long term solution to the
|
||||
problem and went on to design and implement DNS. It's a testament to his
|
||||
genius that the his creation has been able to scale from a few thousand
|
||||
computers to the Internet as we know it today.
|
||||
|
||||
With the combined goal of gaining a deep understanding of DNS, of doing
|
||||
something interesting with Rust, and of scratching some of my own itches,
|
||||
I originally set out to implement my own DNS server. This document is not
|
||||
a truthful chronicle of that journey, but rather an idealized version of it,
|
||||
without all the detours I ended up taking. We'll gradually implement a full
|
||||
DNS server, starting from first principles.
|
||||
|
||||
* [Chapter 1 - The DNS protocol](/src/bin/chapter1.md)
|
||||
* [Chapter 2 - Building a stub resolver](/src/bin/chapter2.md)
|
@ -1,845 +0,0 @@
|
||||
//@ The DNS protocol
|
||||
//@ ----------------
|
||||
//@
|
||||
//@ We'll start out by investigating the DNS protocol and use our knowledge thereof
|
||||
//@ to implement a simple client.
|
||||
//@
|
||||
//@ Conventionally, DNS packets are sent using UDP transport and are limited to 512
|
||||
//@ bytes. As we'll see later, both of those rules have exceptions: DNS can be used
|
||||
//@ over TCP as well, and using a mechanism known as eDNS we can extend the packet
|
||||
//@ size. For now, we'll stick to the original specification, though.
|
||||
//@
|
||||
//@ DNS is quite convenient in the sense that queries and responses use the same
|
||||
//@ format. This means that once we've written a packet parser and a packet writer,
|
||||
//@ our protocol work is done. This differs from most Internet Protocols, which
|
||||
//@ typically use different request and response structures. On a high level, a DNS
|
||||
//@ packet looks as follows:
|
||||
//@
|
||||
//@ | Section | Size | Type | Purpose |
|
||||
//@ | ------------------ | -------- | ----------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
//@ | Header | 12 Bytes | Header | Information about the query/response. |
|
||||
//@ | Question Section | Variable | List of Questions | In practice only a single question indicating the query name (domain) and the record type of interest. |
|
||||
//@ | Answer Section | Variable | List of Records | The relevant records of the requested type. |
|
||||
//@ | Authority Section | Variable | List of Records | An list of name servers (NS records), used for resolving queries recursively. |
|
||||
//@ | Additional Section | Variable | List of Records | Additional records, that might be useful. For instance, the corresponding A records for NS records. |
|
||||
//@
|
||||
//@ Essentially, we have to support three different objects: Header, Question and
|
||||
//@ Record. Conveniently, the lists of records and questions are simply individual
|
||||
//@ instances appended in a row, with no extras. The number of records in each
|
||||
//@ section is provided by the header. The header structure looks as follows:
|
||||
//@
|
||||
//@ | RFC Name | Descriptive Name | Length | Description |
|
||||
//@ | -------- | -------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
//@ | ID | Packet Identifier | 16 bits | A random identifier is assigned to query packets. Response packets must reply with the same id. This is needed to differentiate responses due to the stateless nature of UDP. |
|
||||
//@ | QR | Query Response | 1 bit | 0 for queries, 1 for responses. |
|
||||
//@ | OPCODE | Operation Code | 4 bits | Typically always 0, see RFC1035 for details. |
|
||||
//@ | AA | Authoritative Answer | 1 bit | Set to 1 if the responding server is authoritative - that is, it "owns" - the domain queried. |
|
||||
//@ | TC | Truncated Message | 1 bit | Set to 1 if the message length exceeds 512 bytes. Traditionally a hint that the query can be reissued using TCP, for which the length limitation doesn't apply. |
|
||||
//@ | RD | Recursion Desired | 1 bit | Set by the sender of the request if the server should attempt to resolve the query recursively if it does not have an answer readily available. |
|
||||
//@ | RA | Recursion Available | 1 bit | Set by the server to indicate whether or not recursive queries are allowed. |
|
||||
//@ | Z | Reserved | 3 bits | Originally reserved for later use, but now used for DNSSEC queries. |
|
||||
//@ | RCODE | Response Code | 4 bits | Set by the server to indicate the status of the response, i.e. whether or not it was successful or failed, and in the latter case providing details about the cause of the failure. |
|
||||
//@ | QDCOUNT | Question Count | 16 bits | The number of entries in the Question Section |
|
||||
//@ | ANCOUNT | Answer Count | 16 bits | The number of entries in the Answer Section |
|
||||
//@ | NSCOUNT | Authority Count | 16 bits | The number of entries in the Authority Section |
|
||||
//@ | ARCOUNT | Additional Count | 16 bits | The number of entries in the Additional Section |
|
||||
//@
|
||||
//@ The question is quite a bit less scary:
|
||||
//@
|
||||
//@ | Field | Type | Description |
|
||||
//@ | ------ | -------------- | -------------------------------------------------------------------- |
|
||||
//@ | Name | Label Sequence | The domain name, encoded as a sequence of labels as described below. |
|
||||
//@ | Type | 2-byte Integer | The record type. |
|
||||
//@ | Class | 2-byte Integer | The class, in practice always set to 1. |
|
||||
//@
|
||||
//@ The tricky part lies in the encoding of the domain name, which we'll return to
|
||||
//@ later.
|
||||
//@
|
||||
//@ Finally, we've got the records which are the meat of the protocol. Many record
|
||||
//@ types exists, but for now we'll only consider a few essential. All records have
|
||||
//@ the following preamble:
|
||||
//@
|
||||
//@ | Field | Type | Description |
|
||||
//@ | ------ | -------------- | --------------------------------------------------------------------------------- |
|
||||
//@ | Name | Label Sequence | The domain name, encoded as a sequence of labels as described below. |
|
||||
//@ | Type | 2-byte Integer | The record type. |
|
||||
//@ | Class | 2-byte Integer | The class, in practice always set to 1. |
|
||||
//@ | TTL | 4-byte Integer | Time-To-Live, i.e. how long a record can be cached before it should be requeried. |
|
||||
//@ | Len | 2-byte Integer | Length of the record type specific data. |
|
||||
//@
|
||||
//@ Now we are all set to look a specific record types, and we'll start with the
|
||||
//@ most essential: the A record, mapping a name to an ip.
|
||||
//@
|
||||
//@ | Field | Type | Description |
|
||||
//@ | ---------- | --------------- | --------------------------------------------------------------------------------- |
|
||||
//@ | Preamble | Record Preamble | The record preamble, as described above, with the length field set to 4. |
|
||||
//@ | IP | 4-byte Integer | An IP-adress encoded as a four byte integer. |
|
||||
//@
|
||||
//@ Having gotten this far, let's get a feel for this in practice by performing
|
||||
//@ a lookup using the `dig` tool:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ # dig +noedns google.com
|
||||
//@
|
||||
//@ ; <<>> DiG 9.10.3-P4-Ubuntu <<>> +noedns google.com
|
||||
//@ ;; global options: +cmd
|
||||
//@ ;; Got answer:
|
||||
//@ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36383
|
||||
//@ ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
|
||||
//@
|
||||
//@ ;; QUESTION SECTION:
|
||||
//@ ;google.com. IN A
|
||||
//@
|
||||
//@ ;; ANSWER SECTION:
|
||||
//@ google.com. 204 IN A 172.217.18.142
|
||||
//@
|
||||
//@ ;; Query time: 0 msec
|
||||
//@ ;; SERVER: 192.168.1.1#53(192.168.1.1)
|
||||
//@ ;; WHEN: Wed Jul 06 13:24:19 CEST 2016
|
||||
//@ ;; MSG SIZE rcvd: 44
|
||||
//@ ```
|
||||
//@
|
||||
//@ We're using the `+noedns` flag to make sure we stick to the original format.
|
||||
//@ There are a few things of note in the output above:
|
||||
//@
|
||||
//@ * We can see that `dig` explicitly describes the header, question and answer
|
||||
//@ sections of the response packet.
|
||||
//@ * The header is using the OPCODE QUERY which corresponds to 0. The status
|
||||
//@ (RESCODE) is set to NOERROR, which is 0 numerically. The id is 36383, and
|
||||
//@ will change randomly with repeated queries. The Query Response (qr),
|
||||
//@ Recursion Desired (rd), Recursion Available (ra). We can ignore `ad` for
|
||||
//@ now, since it relates to DNSSEC. Finally, the header tells us that there is
|
||||
//@ one question and one answer record.
|
||||
//@ * The question section shows us our question, with the `IN` indicating the
|
||||
//@ class, and A telling us that we're performing a query for A records.
|
||||
//@ * The answer section contains the answer record, with googles IP. `204` is the
|
||||
//@ TTL, IN is again the class, and A is the record type. Finally, we've got the
|
||||
//@ google.com IP-adress.
|
||||
//@ * The final line tells us that the total packet size was 44 bytes.
|
||||
//@
|
||||
//@ There are still some details obscured from view here though, so let's dive
|
||||
//@ deeper still and look at a hexdump of the packets. We can use `netcat` to listen
|
||||
//@ on a part, and then direct `dig` to send the query there. In one terminal
|
||||
//@ window we run:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ # nc -u -l 1053 > query_packet.txt
|
||||
//@ ```
|
||||
//@
|
||||
//@ Then in another window, do:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ # dig +retry=0 -p 1053 @127.0.0.1 +noedns google.com
|
||||
//@
|
||||
//@ ; <<>> DiG 9.10.3-P4-Ubuntu <<>> +retry=0 -p 1053 @127.0.0.1 +noedns google.com
|
||||
//@ ; (1 server found)
|
||||
//@ ;; global options: +cmd
|
||||
//@ ;; connection timed out; no servers could be reached
|
||||
//@ ```
|
||||
//@
|
||||
//@ The failure is expected in this case, since `dig` will timeout when it doesn't
|
||||
//@ receive a response. Since this fails, it exits. At this point `netcat` can be
|
||||
//@ exited using Ctrl+C. We're left with a query packet in `packet.txt`. We can use our
|
||||
//@ query packet to record a response packet as well:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ # nc -u 8.8.8.8 53 < query_packet.txt > response_packet.txt
|
||||
//@ ```
|
||||
//@
|
||||
//@ Give it a second, and the cancel using Ctrl+C. We are now ready to inspect our
|
||||
//@ packets:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ # hexdump -C query_packet.txt
|
||||
//@ 00000000 86 2a 01 20 00 01 00 00 00 00 00 00 06 67 6f 6f |.*. .........goo|
|
||||
//@ 00000010 67 6c 65 03 63 6f 6d 00 00 01 00 01 |gle.com.....|
|
||||
//@ 0000001c
|
||||
//@ # hexdump -C response_packet.txt
|
||||
//@ 00000000 86 2a 81 80 00 01 00 01 00 00 00 00 06 67 6f 6f |.*...........goo|
|
||||
//@ 00000010 67 6c 65 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 |gle.com.........|
|
||||
//@ 00000020 00 01 00 00 01 25 00 04 d8 3a d3 8e |.....%...:..|
|
||||
//@ 0000002c
|
||||
//@ ```
|
||||
//@
|
||||
//@ Let's see if we can make some sense of this. We know from earlier that the
|
||||
//@ header is 12 bytes long. For the query packet, the header bytes are:
|
||||
//@ `86 2a 01 20 00 01 00 00 00 00 00 00` We can see that the last eight bytes
|
||||
//@ corresponds to the length of the different sections, with the only one actually
|
||||
//@ having any content being the question section which holds a single entry. The
|
||||
//@ more interesting part is the first four bytes, which corresponds to the
|
||||
//@ different fields of the header. First off, we know that we've got a 2-byte
|
||||
//@ id, which is supposed to stay the same for both query and answer. Indeed we
|
||||
//@ see that in this example it's set to `86 2a` in both hexdumps. The hard part
|
||||
//@ to parse is the remaining two bytes. In order to make sense of them, we'll have
|
||||
//@ to convert them to binary. Starting with the `01 20` of the query packet, we
|
||||
//@ find (with the Most Significant Bit first):
|
||||
//@
|
||||
//@ ```text
|
||||
//@ 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0
|
||||
//@ - -+-+-+- - - - - -+-+- -+-+-+-
|
||||
//@ Q O A T R R Z R
|
||||
//@ R P A C D A C
|
||||
//@ C O
|
||||
//@ O D
|
||||
//@ D E
|
||||
//@ E
|
||||
//@ ```
|
||||
//@
|
||||
//@ Except for the DNSSEC related bit in the `Z` section, this is as expected. `QR`
|
||||
//@ is 0 since its a Query, `OPCODE` is also 0 since it's a standard lookup, the
|
||||
//@ `AA`, `TC` and `RA` flags isn't relevant for queries while `RD` is set, since `dig`
|
||||
//@ defaults to requesting recursive lookup. Finally, `RCODE` isn't used for
|
||||
//@ queries either.
|
||||
//@
|
||||
//@ Moving on to the flag bytes of the response packet `81 80`:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0
|
||||
//@ - -+-+-+- - - - - -+-+- -+-+-+-
|
||||
//@ Q O A T R R Z R
|
||||
//@ R P A C D A C
|
||||
//@ C O
|
||||
//@ O D
|
||||
//@ D E
|
||||
//@ E
|
||||
//@ ```
|
||||
//@
|
||||
//@ Since this is a response `QR` is set, and so is `RA` to indicate that the
|
||||
//@ server do support recursion. Looking at the remaining eight bytes of the reply,
|
||||
//@ we see that in addition to having a single question, we've also got a single
|
||||
//@ answer record.
|
||||
//@
|
||||
//@ Immediately past the header, we've got the question. Let's break it down byte
|
||||
//@ by byte:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ query name type class
|
||||
//@ ----------------------------------- ----- -----
|
||||
//@ HEX 06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 01 00 01
|
||||
//@ ASCII g o o g l e c o m
|
||||
//@ DEC 6 3 0 1 1
|
||||
//@ ```
|
||||
//@
|
||||
//@ As outlined in the table earlier, it consists of three parts: query name, type
|
||||
//@ and class. There's something interesting about the how the name is encoded,
|
||||
//@ though -- there are no dots present. Rather DNS encodes each name into
|
||||
//@ a sequence of `labels`, with each label prepended by a single byte indicating
|
||||
//@ its length. In the example above, "google" is 6 bytes and is thus preceded by
|
||||
//@ `0x06`, while "com" is 3 bytes and is preceded by `0x00`. Finally, all names
|
||||
//@ are terminated by a label of zero length, that is a null byte. Seems easy
|
||||
//@ enough, doesn't it? Well, as we shall see soon there's another twist to it.
|
||||
//@
|
||||
//@ We've now reached the end of our query packet, but there is some data left to
|
||||
//@ decode in the response packet. The remaining data is a single A record holding
|
||||
//@ the corresponding IP address for google.com:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ name type class ttl len ip
|
||||
//@ ------ ------ ------ -------------- ------ --------------
|
||||
//@ HEX c0 0c 00 01 00 01 00 00 01 25 00 04 d8 3a d3 8e
|
||||
//@ DEC 192 12 1 1 293 4 216 58 211 142
|
||||
//@ ```
|
||||
//@
|
||||
//@ Most of this is as expected: Type is 1 for `A record`, Class is 1 for `IN`, TTL
|
||||
//@ in this case is 293 which seems reasonable, the data length is 4 which is as it
|
||||
//@ should, and finally we learn that the IP of google is `216.58.211.142`. What
|
||||
//@ then is going on with the name field? Where are the labels we just learned
|
||||
//@ about?
|
||||
//@
|
||||
//@ Due to the original size constraints of DNS, of 512 bytes for a single packet,
|
||||
//@ some type of compression was needed. Since most of the space required is for
|
||||
//@ the domain names, and part of the same name tends to reoccur, there's some
|
||||
//@ obvious space saving opportunity. For example, consider the following DNS
|
||||
//@ query:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ # dig @a.root-servers.net com
|
||||
//@
|
||||
//@ - snip -
|
||||
//@
|
||||
//@ ;; AUTHORITY SECTION:
|
||||
//@ com. 172800 IN NS e.gtld-servers.net.
|
||||
//@ com. 172800 IN NS b.gtld-servers.net.
|
||||
//@ com. 172800 IN NS j.gtld-servers.net.
|
||||
//@ com. 172800 IN NS m.gtld-servers.net.
|
||||
//@ com. 172800 IN NS i.gtld-servers.net.
|
||||
//@ com. 172800 IN NS f.gtld-servers.net.
|
||||
//@ com. 172800 IN NS a.gtld-servers.net.
|
||||
//@ com. 172800 IN NS g.gtld-servers.net.
|
||||
//@ com. 172800 IN NS h.gtld-servers.net.
|
||||
//@ com. 172800 IN NS l.gtld-servers.net.
|
||||
//@ com. 172800 IN NS k.gtld-servers.net.
|
||||
//@ com. 172800 IN NS c.gtld-servers.net.
|
||||
//@ com. 172800 IN NS d.gtld-servers.net.
|
||||
//@
|
||||
//@ ;; ADDITIONAL SECTION:
|
||||
//@ e.gtld-servers.net. 172800 IN A 192.12.94.30
|
||||
//@ b.gtld-servers.net. 172800 IN A 192.33.14.30
|
||||
//@ b.gtld-servers.net. 172800 IN AAAA 2001:503:231d::2:30
|
||||
//@ j.gtld-servers.net. 172800 IN A 192.48.79.30
|
||||
//@ m.gtld-servers.net. 172800 IN A 192.55.83.30
|
||||
//@ i.gtld-servers.net. 172800 IN A 192.43.172.30
|
||||
//@ f.gtld-servers.net. 172800 IN A 192.35.51.30
|
||||
//@ a.gtld-servers.net. 172800 IN A 192.5.6.30
|
||||
//@ a.gtld-servers.net. 172800 IN AAAA 2001:503:a83e::2:30
|
||||
//@ g.gtld-servers.net. 172800 IN A 192.42.93.30
|
||||
//@ h.gtld-servers.net. 172800 IN A 192.54.112.30
|
||||
//@ l.gtld-servers.net. 172800 IN A 192.41.162.30
|
||||
//@ k.gtld-servers.net. 172800 IN A 192.52.178.30
|
||||
//@ c.gtld-servers.net. 172800 IN A 192.26.92.30
|
||||
//@ d.gtld-servers.net. 172800 IN A 192.31.80.30
|
||||
//@
|
||||
//@ - snip -
|
||||
//@ ```
|
||||
//@
|
||||
//@ Here we query one of the internet root servers for the name servers handling
|
||||
//@ the .com TLD. Notice how `gtld-servers.net.` keeps reappearing -- wouldn't it
|
||||
//@ be convenient if we'd only have to include it once? One way to achieve this is
|
||||
//@ to include a "jump directive", telling the packet parser to jump to another
|
||||
//@ position, and finish reading the name there. As it turns out, that's exactly
|
||||
//@ what we're looking at in our response packet.
|
||||
//@
|
||||
//@ I mentioned earlier that each label is preceeded by a single byte length. The
|
||||
//@ additional thing we need to consider is that if the two Most Significant Bits of
|
||||
//@ the length is set, we can instead expect the length byte to be followed by
|
||||
//@ a second byte. These two bytes taken together, and removing the two MSB's, indicate
|
||||
//@ the jump position. In the example above, we've got `0xC00C`. The bit pattern of
|
||||
//@ the the two high bits expressed as hex is `0xC000` (in binary `11000000
|
||||
//@ 00000000`), so we can find the jump position by xoring our two bytes with this
|
||||
//@ mask to unset them: `0xC00C ^ 0xC000 = 12`. Thus we should jump to byte 12 of
|
||||
//@ the packet and read from there. Recalling that the length the DNS header
|
||||
//@ happens to be 12 bytes, we realize that it's instructing us to start reading
|
||||
//@ from where the question part of the packet begins, which makes sense since the
|
||||
//@ question starts with the query domain which in this case is "google.com". Once
|
||||
//@ we've finished reading the name, we resume parsing where we left of, and move
|
||||
//@ on to the record type.
|
||||
//@
|
||||
//@ ### BytePacketBuffer
|
||||
//@
|
||||
//@ Now finally we know enough to start implementing! The first order of business is
|
||||
//@ that we need some convenient method for manipulating the packets. For this,
|
||||
//@ we'll use a `struct` called `BytePacketBuffer`.
|
||||
|
||||
use std::io::{Result, Read};
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::fs::File;
|
||||
|
||||
pub struct BytePacketBuffer {
|
||||
pub buf: [u8; 512],
|
||||
pub pos: usize
|
||||
}
|
||||
|
||||
impl BytePacketBuffer {
|
||||
|
||||
pub fn new() -> BytePacketBuffer {
|
||||
BytePacketBuffer {
|
||||
buf: [0; 512],
|
||||
pos: 0
|
||||
}
|
||||
}
|
||||
|
||||
//@ This gives us a fresh buffer for holding the packet contents, and a field for
|
||||
//@ keeping track of where we are. When handling the reading of domain names later
|
||||
//@ on, we'll need a way of reading and manipulating our buffer position:
|
||||
|
||||
fn pos(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
fn step(&mut self, steps: usize) -> Result<()> {
|
||||
self.pos += steps;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seek(&mut self, pos: usize) -> Result<()> {
|
||||
self.pos = pos;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//@ Next up is a method for reading a single byte, and moving one step forward:
|
||||
|
||||
fn read(&mut self) -> Result<u8> {
|
||||
if self.pos >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
let res = self.buf[self.pos];
|
||||
self.pos += 1;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
//@ We might also want to retrieve a byte at our current position without moving
|
||||
//@ forward:
|
||||
|
||||
fn get(&mut self, pos: usize) -> Result<u8> {
|
||||
if pos >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
Ok(self.buf[pos])
|
||||
}
|
||||
|
||||
//@ Sometimes we want to get a full range of the buffer:
|
||||
|
||||
fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
|
||||
if start + len >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
Ok(&self.buf[start..start+len as usize])
|
||||
}
|
||||
|
||||
//@ In many cases we'll want to read a two-byte integer or a four-byte integer:
|
||||
|
||||
fn read_u16(&mut self) -> Result<u16>
|
||||
{
|
||||
let res = ((try!(self.read()) as u16) << 8) |
|
||||
(try!(self.read()) as u16);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn read_u32(&mut self) -> Result<u32>
|
||||
{
|
||||
let res = ((try!(self.read()) as u32) << 24) |
|
||||
((try!(self.read()) as u32) << 16) |
|
||||
((try!(self.read()) as u32) << 8) |
|
||||
((try!(self.read()) as u32) << 0);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
//@ And then we get to the tricky part of correctly reading domain names, handling
|
||||
//@ any jump we might encounter:
|
||||
|
||||
fn read_qname(&mut self, outstr: &mut String) -> Result<()>
|
||||
{
|
||||
|
||||
//@ Since we might encounter jumps, we'll keep track of our position
|
||||
//@ locally as opposed to using the position within the struct.
|
||||
|
||||
let mut pos = self.pos();
|
||||
|
||||
//@ We'll keep track of whether or not we've jumped for use later on
|
||||
|
||||
let mut jumped = false;
|
||||
|
||||
//@ Our delimeter which we append for each label. Since we don't want a dot at the
|
||||
//@ beginning of the domain name we'll leave it empty for now and set it to "." at
|
||||
//@ the end of the first iteration.
|
||||
|
||||
let mut delim = "";
|
||||
loop {
|
||||
|
||||
//@ We're at the beginning of a label, so our current position is the length byte.
|
||||
|
||||
let len = try!(self.get(pos));
|
||||
|
||||
//@ `len* is a two byte sequence, where the two highest bits of the first byte is
|
||||
//@ set, represents a offset relative to the start of the buffer. We handle this
|
||||
//@ by jumping to the offset, setting a flag to indicate that we shouldn't update
|
||||
//@ the shared buffer position once done.
|
||||
|
||||
if (len & 0xC0) > 0 {
|
||||
|
||||
//@ When a jump is performed, we only modify the shared buffer position once, and avoid
|
||||
//@ making the change later on.
|
||||
|
||||
if !jumped {
|
||||
try!(self.seek(pos+2));
|
||||
}
|
||||
|
||||
//@ Read another byte, and calculate the jump offset:
|
||||
|
||||
let b2 = try!(self.get(pos+1)) as u16;
|
||||
let offset = (((len as u16) ^ 0xC0) << 8) | b2;
|
||||
pos = offset as usize;
|
||||
|
||||
//@ Indicate that a jump was performed.
|
||||
|
||||
jumped = true;
|
||||
|
||||
//@ Restart the loop and retry at the new position.
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
//@ Move a single byte forward to move past the length byte.
|
||||
|
||||
pos += 1;
|
||||
|
||||
//@ Domain names are terminated by an empty label of length 0, so if the length is zero
|
||||
//@ we're done.
|
||||
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
//@ Append the delimiter to our output buffer first.
|
||||
|
||||
outstr.push_str(delim);
|
||||
|
||||
//@ Extract the actual ASCII bytes for this label and append them to the output buffer.
|
||||
|
||||
let str_buffer = try!(self.get_range(pos, len as usize));
|
||||
outstr.push_str(&String::from_utf8_lossy(str_buffer).to_lowercase());
|
||||
|
||||
delim = ".";
|
||||
|
||||
//@ Move forward the full length of the label.
|
||||
|
||||
pos += len as usize;
|
||||
}
|
||||
|
||||
//@ If a jump has been performed, we've already modified the buffer position state and
|
||||
//@ shouldn't do so again.
|
||||
|
||||
if !jumped {
|
||||
try!(self.seek(pos));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} // End of read_qname
|
||||
|
||||
//@ Oh, and we're done:
|
||||
|
||||
} // End of BytePacketBuffer
|
||||
|
||||
//@ ### ResultCode
|
||||
//@
|
||||
//@ Before we move on to the header, we'll add an enum for the values of `rescode` field:
|
||||
|
||||
#[derive(Copy,Clone,Debug,PartialEq,Eq)]
|
||||
pub enum ResultCode {
|
||||
NOERROR = 0,
|
||||
FORMERR = 1,
|
||||
SERVFAIL = 2,
|
||||
NXDOMAIN = 3,
|
||||
NOTIMP = 4,
|
||||
REFUSED = 5
|
||||
}
|
||||
|
||||
impl ResultCode {
|
||||
pub fn from_num(num: u8) -> ResultCode {
|
||||
match num {
|
||||
1 => ResultCode::FORMERR,
|
||||
2 => ResultCode::SERVFAIL,
|
||||
3 => ResultCode::NXDOMAIN,
|
||||
4 => ResultCode::NOTIMP,
|
||||
5 => ResultCode::REFUSED,
|
||||
0 | _ => ResultCode::NOERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@ ### DnsHeader
|
||||
//@
|
||||
//@ Now we can get to work on the header. We'll represent it like this:
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct DnsHeader {
|
||||
pub id: u16, // 16 bits
|
||||
|
||||
pub recursion_desired: bool, // 1 bit
|
||||
pub truncated_message: bool, // 1 bit
|
||||
pub authoritative_answer: bool, // 1 bit
|
||||
pub opcode: u8, // 4 bits
|
||||
pub response: bool, // 1 bit
|
||||
|
||||
pub rescode: ResultCode, // 4 bits
|
||||
pub checking_disabled: bool, // 1 bit
|
||||
pub authed_data: bool, // 1 bit
|
||||
pub z: bool, // 1 bit
|
||||
pub recursion_available: bool, // 1 bit
|
||||
|
||||
pub questions: u16, // 16 bits
|
||||
pub answers: u16, // 16 bits
|
||||
pub authoritative_entries: u16, // 16 bits
|
||||
pub resource_entries: u16 // 16 bits
|
||||
}
|
||||
|
||||
//@ The implementation involves a lot of bit twiddling:
|
||||
|
||||
impl DnsHeader {
|
||||
pub fn new() -> DnsHeader {
|
||||
DnsHeader { id: 0,
|
||||
|
||||
recursion_desired: false,
|
||||
truncated_message: false,
|
||||
authoritative_answer: false,
|
||||
opcode: 0,
|
||||
response: false,
|
||||
|
||||
rescode: ResultCode::NOERROR,
|
||||
checking_disabled: false,
|
||||
authed_data: false,
|
||||
z: false,
|
||||
recursion_available: false,
|
||||
|
||||
questions: 0,
|
||||
answers: 0,
|
||||
authoritative_entries: 0,
|
||||
resource_entries: 0 }
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buffer: &mut BytePacketBuffer) -> Result<()> {
|
||||
self.id = try!(buffer.read_u16());
|
||||
|
||||
let flags = try!(buffer.read_u16());
|
||||
let a = (flags >> 8) as u8;
|
||||
let b = (flags & 0xFF) as u8;
|
||||
self.recursion_desired = (a & (1 << 0)) > 0;
|
||||
self.truncated_message = (a & (1 << 1)) > 0;
|
||||
self.authoritative_answer = (a & (1 << 2)) > 0;
|
||||
self.opcode = (a >> 3) & 0x0F;
|
||||
self.response = (a & (1 << 7)) > 0;
|
||||
|
||||
self.rescode = ResultCode::from_num(b & 0x0F);
|
||||
self.checking_disabled = (b & (1 << 4)) > 0;
|
||||
self.authed_data = (b & (1 << 5)) > 0;
|
||||
self.z = (b & (1 << 6)) > 0;
|
||||
self.recursion_available = (b & (1 << 7)) > 0;
|
||||
|
||||
self.questions = try!(buffer.read_u16());
|
||||
self.answers = try!(buffer.read_u16());
|
||||
self.authoritative_entries = try!(buffer.read_u16());
|
||||
self.resource_entries = try!(buffer.read_u16());
|
||||
|
||||
// Return the constant header size
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//@ ### QueryType
|
||||
//@
|
||||
//@ Before moving on to the question part of the packet, we'll need a way to
|
||||
//@ represent the record type being queried:
|
||||
|
||||
#[derive(PartialEq,Eq,Debug,Clone,Hash,Copy)]
|
||||
pub enum QueryType {
|
||||
UNKNOWN(u16),
|
||||
A, // 1
|
||||
}
|
||||
|
||||
impl QueryType {
|
||||
pub fn to_num(&self) -> u16 {
|
||||
match *self {
|
||||
QueryType::UNKNOWN(x) => x,
|
||||
QueryType::A => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_num(num: u16) -> QueryType {
|
||||
match num {
|
||||
1 => QueryType::A,
|
||||
_ => QueryType::UNKNOWN(num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@ ### DnsQuestion
|
||||
//@
|
||||
//@ The enum allows us to easily add more record types later on. Now for the
|
||||
//@ question entries:
|
||||
|
||||
#[derive(Debug,Clone,PartialEq,Eq)]
|
||||
pub struct DnsQuestion {
|
||||
pub name: String,
|
||||
pub qtype: QueryType
|
||||
}
|
||||
|
||||
impl DnsQuestion {
|
||||
pub fn new(name: String, qtype: QueryType) -> DnsQuestion {
|
||||
DnsQuestion {
|
||||
name: name,
|
||||
qtype: qtype
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buffer: &mut BytePacketBuffer) -> Result<()> {
|
||||
try!(buffer.read_qname(&mut self.name));
|
||||
self.qtype = QueryType::from_num(try!(buffer.read_u16())); // qtype
|
||||
let _ = try!(buffer.read_u16()); // class
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//@ Having done the hard part of reading the domain names as part of our
|
||||
//@ `BytePacketBuffer` struct, it turns out to be quite compact.
|
||||
//@
|
||||
//@ ### DnsRecord
|
||||
//@
|
||||
//@ We'll obviously need a way of representing the actual dns records as well, and
|
||||
//@ again we'll use an enum for easy expansion:
|
||||
|
||||
#[derive(Debug,Clone,PartialEq,Eq,Hash,PartialOrd,Ord)]
|
||||
#[allow(dead_code)]
|
||||
pub enum DnsRecord {
|
||||
UNKNOWN {
|
||||
domain: String,
|
||||
qtype: u16,
|
||||
data_len: u16,
|
||||
ttl: u32
|
||||
}, // 0
|
||||
A {
|
||||
domain: String,
|
||||
addr: Ipv4Addr,
|
||||
ttl: u32
|
||||
}, // 1
|
||||
}
|
||||
|
||||
//@ Since there are many types of records, we'll add the ability to keep track of
|
||||
//@ record types we haven't yet encountered. The enum will also allow us to easily
|
||||
//@ add new records later on. The actual implementation of `DnsRecord` looks like
|
||||
//@ this:
|
||||
|
||||
impl DnsRecord {
|
||||
|
||||
pub fn read(buffer: &mut BytePacketBuffer) -> Result<DnsRecord> {
|
||||
let mut domain = String::new();
|
||||
try!(buffer.read_qname(&mut domain));
|
||||
|
||||
let qtype_num = try!(buffer.read_u16());
|
||||
let qtype = QueryType::from_num(qtype_num);
|
||||
let _ = try!(buffer.read_u16()); // class, which we ignore
|
||||
let ttl = try!(buffer.read_u32());
|
||||
let data_len = try!(buffer.read_u16());
|
||||
|
||||
match qtype {
|
||||
QueryType::A => {
|
||||
let raw_addr = try!(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
|
||||
})
|
||||
},
|
||||
QueryType::UNKNOWN(_) => {
|
||||
try!(buffer.step(data_len as usize));
|
||||
|
||||
Ok(DnsRecord::UNKNOWN {
|
||||
domain: domain,
|
||||
qtype: qtype_num,
|
||||
data_len: data_len,
|
||||
ttl: ttl
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@ ### DnsPacket
|
||||
//@
|
||||
//@ Finally, we're going to compose them in a struct called `DnsPacket`:
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DnsPacket {
|
||||
pub header: DnsHeader,
|
||||
pub questions: Vec<DnsQuestion>,
|
||||
pub answers: Vec<DnsRecord>,
|
||||
pub authorities: Vec<DnsRecord>,
|
||||
pub resources: Vec<DnsRecord>
|
||||
}
|
||||
|
||||
impl DnsPacket {
|
||||
pub fn new() -> DnsPacket {
|
||||
DnsPacket {
|
||||
header: DnsHeader::new(),
|
||||
questions: Vec::new(),
|
||||
answers: Vec::new(),
|
||||
authorities: Vec::new(),
|
||||
resources: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_buffer(buffer: &mut BytePacketBuffer) -> Result<DnsPacket> {
|
||||
let mut result = DnsPacket::new();
|
||||
try!(result.header.read(buffer));
|
||||
|
||||
for _ in 0..result.header.questions {
|
||||
let mut question = DnsQuestion::new("".to_string(),
|
||||
QueryType::UNKNOWN(0));
|
||||
try!(question.read(buffer));
|
||||
result.questions.push(question);
|
||||
}
|
||||
|
||||
for _ in 0..result.header.answers {
|
||||
let rec = try!(DnsRecord::read(buffer));
|
||||
result.answers.push(rec);
|
||||
}
|
||||
for _ in 0..result.header.authoritative_entries {
|
||||
let rec = try!(DnsRecord::read(buffer));
|
||||
result.authorities.push(rec);
|
||||
}
|
||||
for _ in 0..result.header.resource_entries {
|
||||
let rec = try!(DnsRecord::read(buffer));
|
||||
result.resources.push(rec);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
//@ ### Putting it all together
|
||||
//@
|
||||
//@ Let's use the `response_packet.txt` we generated earlier to try it out!
|
||||
|
||||
fn main() {
|
||||
let mut f = File::open("response_packet.txt").unwrap();
|
||||
let mut buffer = BytePacketBuffer::new();
|
||||
f.read(&mut buffer.buf).unwrap();
|
||||
|
||||
let packet = DnsPacket::from_buffer(&mut buffer).unwrap();
|
||||
println!("{:?}", packet.header);
|
||||
|
||||
for q in packet.questions {
|
||||
println!("{:?}", q);
|
||||
}
|
||||
for rec in packet.answers {
|
||||
println!("{:?}", rec);
|
||||
}
|
||||
for rec in packet.authorities {
|
||||
println!("{:?}", rec);
|
||||
}
|
||||
for rec in packet.resources {
|
||||
println!("{:?}", rec);
|
||||
}
|
||||
}
|
||||
|
||||
//@ Running it will print:
|
||||
//@
|
||||
//@ ```text
|
||||
//@ DnsHeader {
|
||||
//@ id: 34346,
|
||||
//@ recursion_desired: true,
|
||||
//@ truncated_message: false,
|
||||
//@ authoritative_answer: false,
|
||||
//@ opcode: 0,
|
||||
//@ response: true,
|
||||
//@ rescode: NOERROR,
|
||||
//@ checking_disabled: false,
|
||||
//@ authed_data: false,
|
||||
//@ z: false,
|
||||
//@ recursion_available: true,
|
||||
//@ questions: 1,
|
||||
//@ answers: 1,
|
||||
//@ authoritative_entries: 0,
|
||||
//@ resource_entries: 0
|
||||
//@ }
|
||||
//@ DnsQuestion {
|
||||
//@ name: "google.com",
|
||||
//@ qtype: A
|
||||
//@ }
|
||||
//@ A {
|
||||
//@ domain: "google.com",
|
||||
//@ addr: 216.58.211.142,
|
||||
//@ ttl: 293
|
||||
//@ }
|
||||
//@ ```
|
338
src/bin/chapter2.md
Normal file
338
src/bin/chapter2.md
Normal file
@ -0,0 +1,338 @@
|
||||
Building a stub resolver
|
||||
------------------------
|
||||
|
||||
While it's slightly satisfying to know that we're able to succesfully parse DNS
|
||||
packets, it's not much use to just read them off disk. As our next step, we'll
|
||||
use it to build a `stub resolver`, which is a DNS client that doesn't feature
|
||||
any built-in support for recursive lookup and that will only work with a DNS
|
||||
server that does. Later we'll implement an actual recursive resolver to lose
|
||||
the need for a server.
|
||||
|
||||
### Extending BytePacketBuffer for writing
|
||||
|
||||
In order to be able to service a query, we need to be able to not just read
|
||||
packets, but also write them. To do so, we'll need to extend `BytePacketBuffer`
|
||||
with some additional methods:
|
||||
|
||||
```rust
|
||||
extern crate dnsguide;
|
||||
use dnsguide::chapter2::*;
|
||||
```
|
||||
|
||||
```rust
|
||||
trait WriteableBuffer {
|
||||
fn write(&mut self, val: u8) -> Result<usize>;
|
||||
fn write_u8(&mut self, val: u8) -> Result<usize>;
|
||||
fn write_u16(&mut self, val: u16) -> Result<usize>;
|
||||
fn write_u32(&mut self, val: u32) -> Result<usize>;
|
||||
fn write_qname(&mut self, qname: &str) -> Result<usize>;
|
||||
}
|
||||
|
||||
impl WriteableBuffer for BytePacketBuffer {
|
||||
|
||||
fn write(&mut self, val: u8) -> Result<usize> {
|
||||
if self.pos >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
self.buf[self.pos] = val;
|
||||
self.pos += 1;
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
fn write_u8(&mut self, val: u8) -> Result<usize> {
|
||||
try!(self.write(val));
|
||||
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
fn write_u16(&mut self, val: u16) -> Result<usize> {
|
||||
try!(self.write((val >> 8) as u8));
|
||||
try!(self.write((val & 0xFF) as u8));
|
||||
|
||||
Ok(2)
|
||||
}
|
||||
|
||||
fn write_u32(&mut self, val: u32) -> Result<usize> {
|
||||
try!(self.write(((val >> 24) & 0xFF) as u8));
|
||||
try!(self.write(((val >> 16) & 0xFF) as u8));
|
||||
try!(self.write(((val >> 8) & 0xFF) as u8));
|
||||
try!(self.write(((val >> 0) & 0xFF) as u8));
|
||||
|
||||
Ok(4)
|
||||
}
|
||||
```
|
||||
|
||||
We'll also need a function for writing query names in labeled form:
|
||||
|
||||
```rust
|
||||
fn write_qname(&mut self, qname: &str) -> Result<usize> {
|
||||
|
||||
let split_str = qname.split('.').collect::<Vec<&str>>();
|
||||
|
||||
let mut bytes_written = 0;
|
||||
for label in split_str {
|
||||
let len = label.len();
|
||||
if len > 0x34 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "Single label exceeds 63 characters of length"));
|
||||
}
|
||||
|
||||
try!(self.write_u8(len as u8));
|
||||
bytes_written += 1;
|
||||
|
||||
for b in label.as_bytes() {
|
||||
try!(self.write_u8(*b));
|
||||
bytes_written += 1;
|
||||
}
|
||||
}
|
||||
|
||||
try!(self.write_u8(0));
|
||||
bytes_written += 1;
|
||||
|
||||
Ok(bytes_written)
|
||||
}
|
||||
|
||||
} // End of BytePacketBuffer
|
||||
```
|
||||
|
||||
### Extending DnsHeader for writing
|
||||
|
||||
Building on our new functions we can extend our protocol representation
|
||||
structs. Starting with `DnsHeader`:
|
||||
|
||||
```rust
|
||||
trait BufferWriteable {
|
||||
fn write(&self, buffer: &mut BytePacketBuffer) -> Result<usize>;
|
||||
}
|
||||
|
||||
impl BufferWriteable for DnsHeader {
|
||||
|
||||
fn write(&self, buffer: &mut BytePacketBuffer) -> Result<usize> {
|
||||
try!(buffer.write_u16(self.id));
|
||||
|
||||
try!(buffer.write_u8( ((self.recursion_desired as u8)) |
|
||||
((self.truncated_message as u8) << 1) |
|
||||
((self.authoritative_answer as u8) << 2) |
|
||||
(self.opcode << 3) |
|
||||
((self.response as u8) << 7) as u8) );
|
||||
|
||||
try!(buffer.write_u8( (self.rescode.clone() as u8) |
|
||||
((self.checking_disabled as u8) << 4) |
|
||||
((self.authed_data as u8) << 5) |
|
||||
((self.z as u8) << 6) |
|
||||
((self.recursion_available as u8) << 7) ));
|
||||
|
||||
try!(buffer.write_u16(self.questions));
|
||||
try!(buffer.write_u16(self.answers));
|
||||
try!(buffer.write_u16(self.authoritative_entries));
|
||||
try!(buffer.write_u16(self.resource_entries));
|
||||
|
||||
Ok(12)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Extending DnsQuestion for writing
|
||||
|
||||
Moving on to `DnsQuestion`:
|
||||
|
||||
```rust
|
||||
impl BufferWriteable for DnsQuestion {
|
||||
|
||||
fn write(&self, buffer: &mut BytePacketBuffer) -> Result<usize> {
|
||||
|
||||
let mut bytes_written = 0;
|
||||
|
||||
bytes_written += try!(buffer.write_qname(&self.name));
|
||||
|
||||
let typenum = self.qtype.to_num();
|
||||
bytes_written += try!(buffer.write_u16(typenum));
|
||||
bytes_written += try!(buffer.write_u16(1));
|
||||
|
||||
Ok(bytes_written)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Extending DnsRecord for writing
|
||||
|
||||
`DnsRecord` is for now quite compact as well, although we'll eventually add
|
||||
quite a bit of code here to handle different record types:
|
||||
|
||||
```rust
|
||||
impl BufferWriteable for DnsRecord {
|
||||
|
||||
fn write(&self, buffer: &mut BytePacketBuffer) -> Result<usize> {
|
||||
|
||||
let start_pos = buffer.pos();
|
||||
|
||||
match *self {
|
||||
DnsRecord::A { ref domain, ref addr, ttl } => {
|
||||
try!(buffer.write_qname(domain));
|
||||
try!(buffer.write_u16(QueryType::A.to_num()));
|
||||
try!(buffer.write_u16(1));
|
||||
try!(buffer.write_u32(ttl));
|
||||
try!(buffer.write_u16(4));
|
||||
|
||||
let octets = addr.octets();
|
||||
try!(buffer.write_u8(octets[0]));
|
||||
try!(buffer.write_u8(octets[1]));
|
||||
try!(buffer.write_u8(octets[2]));
|
||||
try!(buffer.write_u8(octets[3]));
|
||||
},
|
||||
DnsRecord::UNKNOWN { .. } => {
|
||||
println!("Skipping record: {:?}", self);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(buffer.pos() - start_pos)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Extending DnsPacket for writing
|
||||
|
||||
Based on what we've accomplished, we can amend `DnsPacket` with its own
|
||||
`write` function:
|
||||
|
||||
```rust
|
||||
impl BufferWriteable for DnsPacket {
|
||||
|
||||
fn write(&self, buffer: &mut BytePacketBuffer) -> Result<usize>
|
||||
{
|
||||
let mut header = self.header.clone();
|
||||
header.questions = self.questions.len() as u16;
|
||||
header.answers = self.answers.len() as u16;
|
||||
header.authoritative_entries = self.authorities.len() as u16;
|
||||
header.resource_entries = self.resources.len() as u16;
|
||||
|
||||
let mut bytes_written = 0;
|
||||
|
||||
bytes_written += try!(header.write(buffer));
|
||||
|
||||
for question in &self.questions {
|
||||
bytes_written += try!(question.write(buffer));
|
||||
}
|
||||
for rec in &self.answers {
|
||||
bytes_written += try!(rec.write(buffer));
|
||||
}
|
||||
for rec in &self.authorities {
|
||||
bytes_written += try!(rec.write(buffer));
|
||||
}
|
||||
for rec in &self.resources {
|
||||
bytes_written += try!(rec.write(buffer));
|
||||
}
|
||||
|
||||
Ok(bytes_written)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
// Perform an A query for google.com
|
||||
let qname = "google.com";
|
||||
let qtype = QueryType::A;
|
||||
|
||||
// Using googles public DNS server
|
||||
let server = ("8.8.8.8", 53);
|
||||
|
||||
// Bind a UDP socket to an arbitrary port
|
||||
let socket = UdpSocket::bind(("0.0.0.0", 43210)).unwrap();
|
||||
```
|
||||
|
||||
Next we'll build our query packet. It's important that we remember to set the
|
||||
`recursion_desired` flag. As noted earlier, the packet id is arbitrary.
|
||||
|
||||
```rust
|
||||
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));
|
||||
```
|
||||
|
||||
We can use our new write method to write the packet to a buffer...
|
||||
|
||||
```rust
|
||||
let mut req_buffer = BytePacketBuffer::new();
|
||||
packet.write(&mut req_buffer).unwrap();
|
||||
```
|
||||
|
||||
...and send it off to the server using our socket:
|
||||
|
||||
```rust
|
||||
socket.send_to(&req_buffer.buf[0..req_buffer.pos], server).unwrap();
|
||||
```
|
||||
|
||||
To prepare for receiving the response, we'll create a new `BytePacketBuffer`.
|
||||
We'll then ask the socket to write the response directly into our buffer.
|
||||
|
||||
```rust
|
||||
let mut res_buffer = BytePacketBuffer::new();
|
||||
socket.recv_from(&mut res_buffer.buf).unwrap();
|
||||
```
|
||||
|
||||
As per the previous section, `DnsPacket::from_buffer()` is then used to
|
||||
actually parse the packet after which we can print the response.
|
||||
|
||||
```rust
|
||||
let res_packet = DnsPacket::from_buffer(&mut res_buffer).unwrap();
|
||||
println!("{:?}", res_packet.header);
|
||||
|
||||
for q in res_packet.questions {
|
||||
println!("{:?}", q);
|
||||
}
|
||||
for rec in res_packet.answers {
|
||||
println!("{:?}", rec);
|
||||
}
|
||||
for rec in res_packet.authorities {
|
||||
println!("{:?}", rec);
|
||||
}
|
||||
for rec in res_packet.resources {
|
||||
println!("{:?}", rec);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Running it will print:
|
||||
|
||||
```text
|
||||
DnsHeader {
|
||||
id: 6666,
|
||||
recursion_desired: true,
|
||||
truncated_message: false,
|
||||
authoritative_answer: false,
|
||||
opcode: 0,
|
||||
response: true,
|
||||
rescode: NOERROR,
|
||||
checking_disabled: false,
|
||||
authed_data: false,
|
||||
z: false,
|
||||
recursion_available: true,
|
||||
questions: 1,
|
||||
answers: 1,
|
||||
authoritative_entries: 0,
|
||||
resource_entries: 0
|
||||
}
|
||||
DnsQuestion {
|
||||
name: "google.com",
|
||||
qtype: A
|
||||
}
|
||||
A {
|
||||
domain: "google.com",
|
||||
addr: 216.58.209.110,
|
||||
ttl: 79
|
||||
}
|
||||
```
|
||||
|
||||
We're approaching something useful!
|
372
src/chapter2.rs
Normal file
372
src/chapter2.rs
Normal file
@ -0,0 +1,372 @@
|
||||
pub use std::io::{Result, Read};
|
||||
pub use std::io::{Error, ErrorKind};
|
||||
pub use std::net::Ipv4Addr;
|
||||
pub use std::net::UdpSocket;
|
||||
|
||||
pub struct BytePacketBuffer {
|
||||
pub buf: [u8; 512],
|
||||
pub pos: usize
|
||||
}
|
||||
|
||||
impl BytePacketBuffer {
|
||||
pub fn new() -> BytePacketBuffer {
|
||||
BytePacketBuffer {
|
||||
buf: [0; 512],
|
||||
pos: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pos(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
pub fn step(&mut self, steps: usize) -> Result<()> {
|
||||
self.pos += steps;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn seek(&mut self, pos: usize) -> Result<()> {
|
||||
self.pos = pos;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Result<u8> {
|
||||
if self.pos >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
let res = self.buf[self.pos];
|
||||
self.pos += 1;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get(&mut self, pos: usize) -> Result<u8> {
|
||||
if pos >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
Ok(self.buf[pos])
|
||||
}
|
||||
|
||||
pub fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
|
||||
if start + len >= 512 {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "End of buffer"));
|
||||
}
|
||||
Ok(&self.buf[start..start+len as usize])
|
||||
}
|
||||
|
||||
pub fn read_u16(&mut self) -> Result<u16>
|
||||
{
|
||||
let res = ((try!(self.read()) as u16) << 8) |
|
||||
(try!(self.read()) as u16);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn read_u32(&mut self) -> Result<u32>
|
||||
{
|
||||
let res = ((try!(self.read()) as u32) << 24) |
|
||||
((try!(self.read()) as u32) << 16) |
|
||||
((try!(self.read()) as u32) << 8) |
|
||||
((try!(self.read()) as u32) << 0);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn read_qname(&mut self, outstr: &mut String) -> Result<()>
|
||||
{
|
||||
let mut pos = self.pos();
|
||||
let mut jumped = false;
|
||||
|
||||
let mut delim = "";
|
||||
loop {
|
||||
let len = try!(self.get(pos));
|
||||
|
||||
// A two byte sequence, where the two highest bits of the first byte is
|
||||
// set, represents a offset relative to the start of the buffer. We
|
||||
// handle this by jumping to the offset, setting a flag to indicate
|
||||
// that we shouldn't update the shared buffer position once done.
|
||||
if (len & 0xC0) > 0 {
|
||||
|
||||
// When a jump is performed, we only modify the shared buffer
|
||||
// position once, and avoid making the change later on.
|
||||
if !jumped {
|
||||
try!(self.seek(pos+2));
|
||||
}
|
||||
|
||||
let b2 = try!(self.get(pos+1)) as u16;
|
||||
let offset = (((len as u16) ^ 0xC0) << 8) | b2;
|
||||
pos = offset as usize;
|
||||
jumped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
|
||||
// Names are terminated by an empty label of length 0
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
outstr.push_str(delim);
|
||||
|
||||
let str_buffer = try!(self.get_range(pos, len as usize));
|
||||
outstr.push_str(&String::from_utf8_lossy(str_buffer).to_lowercase());
|
||||
|
||||
delim = ".";
|
||||
|
||||
pos += len as usize;
|
||||
}
|
||||
|
||||
if !jumped {
|
||||
try!(self.seek(pos));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone,Debug,PartialEq,Eq)]
|
||||
pub enum ResultCode {
|
||||
NOERROR = 0,
|
||||
FORMERR = 1,
|
||||
SERVFAIL = 2,
|
||||
NXDOMAIN = 3,
|
||||
NOTIMP = 4,
|
||||
REFUSED = 5
|
||||
}
|
||||
|
||||
impl ResultCode {
|
||||
pub fn from_num(num: u8) -> ResultCode {
|
||||
match num {
|
||||
1 => ResultCode::FORMERR,
|
||||
2 => ResultCode::SERVFAIL,
|
||||
3 => ResultCode::NXDOMAIN,
|
||||
4 => ResultCode::NOTIMP,
|
||||
5 => ResultCode::REFUSED,
|
||||
0 | _ => ResultCode::NOERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct DnsHeader {
|
||||
pub id: u16, // 16 bits
|
||||
|
||||
pub recursion_desired: bool, // 1 bit
|
||||
pub truncated_message: bool, // 1 bit
|
||||
pub authoritative_answer: bool, // 1 bit
|
||||
pub opcode: u8, // 4 bits
|
||||
pub response: bool, // 1 bit
|
||||
|
||||
pub rescode: ResultCode, // 4 bits
|
||||
pub checking_disabled: bool, // 1 bit
|
||||
pub authed_data: bool, // 1 bit
|
||||
pub z: bool, // 1 bit
|
||||
pub recursion_available: bool, // 1 bit
|
||||
|
||||
pub questions: u16, // 16 bits
|
||||
pub answers: u16, // 16 bits
|
||||
pub authoritative_entries: u16, // 16 bits
|
||||
pub resource_entries: u16 // 16 bits
|
||||
}
|
||||
|
||||
impl DnsHeader {
|
||||
pub fn new() -> DnsHeader {
|
||||
DnsHeader { id: 0,
|
||||
|
||||
recursion_desired: false,
|
||||
truncated_message: false,
|
||||
authoritative_answer: false,
|
||||
opcode: 0,
|
||||
response: false,
|
||||
|
||||
rescode: ResultCode::NOERROR,
|
||||
checking_disabled: false,
|
||||
authed_data: false,
|
||||
z: false,
|
||||
recursion_available: false,
|
||||
|
||||
questions: 0,
|
||||
answers: 0,
|
||||
authoritative_entries: 0,
|
||||
resource_entries: 0 }
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buffer: &mut BytePacketBuffer) -> Result<()> {
|
||||
self.id = try!(buffer.read_u16());
|
||||
|
||||
let flags = try!(buffer.read_u16());
|
||||
let a = (flags >> 8) as u8;
|
||||
let b = (flags & 0xFF) as u8;
|
||||
self.recursion_desired = (a & (1 << 0)) > 0;
|
||||
self.truncated_message = (a & (1 << 1)) > 0;
|
||||
self.authoritative_answer = (a & (1 << 2)) > 0;
|
||||
self.opcode = (a >> 3) & 0x0F;
|
||||
self.response = (a & (1 << 7)) > 0;
|
||||
|
||||
self.rescode = ResultCode::from_num(b & 0x0F);
|
||||
self.checking_disabled = (b & (1 << 4)) > 0;
|
||||
self.authed_data = (b & (1 << 5)) > 0;
|
||||
self.z = (b & (1 << 6)) > 0;
|
||||
self.recursion_available = (b & (1 << 7)) > 0;
|
||||
|
||||
self.questions = try!(buffer.read_u16());
|
||||
self.answers = try!(buffer.read_u16());
|
||||
self.authoritative_entries = try!(buffer.read_u16());
|
||||
self.resource_entries = try!(buffer.read_u16());
|
||||
|
||||
// Return the constant header size
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq,Eq,Debug,Clone,Hash,Copy)]
|
||||
pub enum QueryType {
|
||||
UNKNOWN(u16),
|
||||
A, // 1
|
||||
}
|
||||
|
||||
impl QueryType {
|
||||
pub fn to_num(&self) -> u16 {
|
||||
match *self {
|
||||
QueryType::UNKNOWN(x) => x,
|
||||
QueryType::A => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_num(num: u16) -> QueryType {
|
||||
match num {
|
||||
1 => QueryType::A,
|
||||
_ => QueryType::UNKNOWN(num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,PartialEq,Eq)]
|
||||
pub struct DnsQuestion {
|
||||
pub name: String,
|
||||
pub qtype: QueryType
|
||||
}
|
||||
|
||||
impl DnsQuestion {
|
||||
pub fn new(name: String, qtype: QueryType) -> DnsQuestion {
|
||||
DnsQuestion {
|
||||
name: name,
|
||||
qtype: qtype
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buffer: &mut BytePacketBuffer) -> Result<()> {
|
||||
try!(buffer.read_qname(&mut self.name));
|
||||
self.qtype = QueryType::from_num(try!(buffer.read_u16())); // qtype
|
||||
let _ = try!(buffer.read_u16()); // class
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,PartialEq,Eq,Hash,PartialOrd,Ord)]
|
||||
#[allow(dead_code)]
|
||||
pub enum DnsRecord {
|
||||
UNKNOWN {
|
||||
domain: String,
|
||||
qtype: u16,
|
||||
data_len: u16,
|
||||
ttl: u32
|
||||
}, // 0
|
||||
A {
|
||||
domain: String,
|
||||
addr: Ipv4Addr,
|
||||
ttl: u32
|
||||
}, // 1
|
||||
}
|
||||
|
||||
impl DnsRecord {
|
||||
|
||||
pub fn read(buffer: &mut BytePacketBuffer) -> Result<DnsRecord> {
|
||||
let mut domain = String::new();
|
||||
try!(buffer.read_qname(&mut domain));
|
||||
|
||||
let qtype_num = try!(buffer.read_u16());
|
||||
let qtype = QueryType::from_num(qtype_num);
|
||||
let _ = try!(buffer.read_u16());
|
||||
let ttl = try!(buffer.read_u32());
|
||||
let data_len = try!(buffer.read_u16());
|
||||
|
||||
match qtype {
|
||||
QueryType::A => {
|
||||
let raw_addr = try!(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
|
||||
})
|
||||
},
|
||||
QueryType::UNKNOWN(_) => {
|
||||
try!(buffer.step(data_len as usize));
|
||||
|
||||
Ok(DnsRecord::UNKNOWN {
|
||||
domain: domain,
|
||||
qtype: qtype_num,
|
||||
data_len: data_len,
|
||||
ttl: ttl
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DnsPacket {
|
||||
pub header: DnsHeader,
|
||||
pub questions: Vec<DnsQuestion>,
|
||||
pub answers: Vec<DnsRecord>,
|
||||
pub authorities: Vec<DnsRecord>,
|
||||
pub resources: Vec<DnsRecord>
|
||||
}
|
||||
|
||||
impl DnsPacket {
|
||||
pub fn new() -> DnsPacket {
|
||||
DnsPacket {
|
||||
header: DnsHeader::new(),
|
||||
questions: Vec::new(),
|
||||
answers: Vec::new(),
|
||||
authorities: Vec::new(),
|
||||
resources: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_buffer(buffer: &mut BytePacketBuffer) -> Result<DnsPacket> {
|
||||
let mut result = DnsPacket::new();
|
||||
try!(result.header.read(buffer));
|
||||
|
||||
for _ in 0..result.header.questions {
|
||||
let mut question = DnsQuestion::new("".to_string(),
|
||||
QueryType::UNKNOWN(0));
|
||||
try!(question.read(buffer));
|
||||
result.questions.push(question);
|
||||
}
|
||||
|
||||
for _ in 0..result.header.answers {
|
||||
let rec = try!(DnsRecord::read(buffer));
|
||||
result.answers.push(rec);
|
||||
}
|
||||
for _ in 0..result.header.authoritative_entries {
|
||||
let rec = try!(DnsRecord::read(buffer));
|
||||
result.authorities.push(rec);
|
||||
}
|
||||
for _ in 0..result.header.resource_entries {
|
||||
let rec = try!(DnsRecord::read(buffer));
|
||||
result.resources.push(rec);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user