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-address 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:
# 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) flags are enabled, which are
1 numerically. 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 google's IP.
204
is the TTL, IN is again the class, and A is the record type. Finally, we've got the google.com IP-address. - 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 port, and then direct dig
to send the query there. In one terminal
window we run:
# nc -u -l 1053 > query_packet.txt
Then in another window, do:
# 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 query_packet.txt
. We can use our
query packet to record a response packet as well:
# 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:
# 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):
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
:
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 does 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:
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 0x03
. 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:
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:
# 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 off, and move
on to the record type.
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
.
pub struct BytePacketBuffer {
pub buf: [u8; 512],
pub pos: usize,
}
impl BytePacketBuffer {
/// This gives us a fresh buffer for holding the packet contents, and a
/// field for keeping track of where we are.
pub fn new() -> BytePacketBuffer {
BytePacketBuffer {
buf: [0; 512],
pos: 0,
}
}
/// Current position within buffer
fn pos(&self) -> usize {
self.pos
}
/// Step the buffer position forward a specific number of steps
fn step(&mut self, steps: usize) -> Result<()> {
self.pos += steps;
Ok(())
}
/// Change the buffer position
fn seek(&mut self, pos: usize) -> Result<()> {
self.pos = pos;
Ok(())
}
/// Read a single byte and move the position one step forward
fn read(&mut self) -> Result<u8> {
if self.pos >= 512 {
return Err("End of buffer".into());
}
let res = self.buf[self.pos];
self.pos += 1;
Ok(res)
}
/// Get a single byte, without changing the buffer position
fn get(&mut self, pos: usize) -> Result<u8> {
if pos >= 512 {
return Err("End of buffer".into());
}
Ok(self.buf[pos])
}
/// Get a range of bytes
fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
if start + len >= 512 {
return Err("End of buffer".into());
}
Ok(&self.buf[start..start + len as usize])
}
/// Read two bytes, stepping two steps forward
fn read_u16(&mut self) -> Result<u16> {
let res = ((self.read()? as u16) << 8) | (self.read()? as u16);
Ok(res)
}
/// Read four bytes, stepping four steps forward
fn read_u32(&mut self) -> Result<u32> {
let res = ((self.read()? as u32) << 24)
| ((self.read()? as u32) << 16)
| ((self.read()? as u32) << 8)
| ((self.read()? as u32) << 0);
Ok(res)
}
/// Read a qname
///
/// The tricky part: Reading domain names, taking labels into consideration.
/// Will take something like [3]www[6]google[3]com[0] and append
/// www.google.com to outstr.
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. This
// allows us to move the shared position to a point past our current
// qname, while keeping track of our progress on the current qname
// using this variable.
let mut pos = self.pos();
// track whether or not we've jumped
let mut jumped = false;
let max_jumps = 5;
let mut jumps_performed = 0;
// Our delimiter 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 {
// Dns Packets are untrusted data, so we need to be paranoid. Someone
// can craft a packet with a cycle in the jump instructions. This guards
// against such packets.
if jumps_performed > max_jumps {
return Err(format!("Limit of {} jumps exceeded", max_jumps).into());
}
// At this point, we're always at the beginning of a label. Recall
// that labels start with a length byte.
let len = self.get(pos)?;
// If len has the two most significant bit are set, it represents a
// jump to some other offset in the packet:
if (len & 0xC0) == 0xC0 {
// Update the buffer position to a point past the current
// label. We don't need to touch it any further.
if !jumped {
self.seek(pos + 2)?;
}
// Read another byte, calculate offset and perform the jump by
// updating our local position variable
let b2 = 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;
jumps_performed += 1;
continue;
}
// The base scenario, where we're reading a single label and
// appending it to the output:
else {
// 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 = 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 !jumped {
self.seek(pos)?;
}
Ok(())
}
}
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,
}
}
}
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 = buffer.read_u16()?;
let flags = 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 = buffer.read_u16()?;
self.answers = buffer.read_u16()?;
self.authoritative_entries = buffer.read_u16()?;
self.resource_entries = buffer.read_u16()?;
Ok(())
}
}
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),
}
}
}
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<()> {
buffer.read_qname(&mut self.name)?;
self.qtype = QueryType::from_num(buffer.read_u16()?); // qtype
let _ = 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.
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();
buffer.read_qname(&mut domain)?;
let qtype_num = buffer.read_u16()?;
let qtype = QueryType::from_num(qtype_num);
let _ = buffer.read_u16()?;
let ttl = buffer.read_u32()?;
let data_len = buffer.read_u16()?;
match qtype {
QueryType::A => {
let raw_addr = 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(_) => {
buffer.step(data_len as usize)?;
Ok(DnsRecord::UNKNOWN {
domain: domain,
qtype: qtype_num,
data_len: data_len,
ttl: ttl,
})
}
}
}
}
Finally, let's put it all together 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();
result.header.read(buffer)?;
for _ in 0..result.header.questions {
let mut question = DnsQuestion::new("".to_string(), QueryType::UNKNOWN(0));
question.read(buffer)?;
result.questions.push(question);
}
for _ in 0..result.header.answers {
let rec = DnsRecord::read(buffer)?;
result.answers.push(rec);
}
for _ in 0..result.header.authoritative_entries {
let rec = DnsRecord::read(buffer)?;
result.authorities.push(rec);
}
for _ in 0..result.header.resource_entries {
let rec = DnsRecord::read(buffer)?;
result.resources.push(rec);
}
Ok(result)
}
}
Let's use the response_packet.txt
we generated earlier to try it out!
fn main() -> Result<()> {
let mut f = File::open("response_packet.txt")?;
let mut buffer = BytePacketBuffer::new();
f.read(&mut buffer.buf)?;
let packet = DnsPacket::from_buffer(&mut buffer)?;
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);
}
Ok(())
}
Running it will print:
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
}
In the next chapter, we'll add network connectivity: Chapter 2 - Building a stub resolver