This is a DNS tool I wrote, based off example code I found here. It creates DNS packets from scratch and sends the queries to a DNS server.
It is a work in progress, and will be updated.
- Output in text, JSON, and pretty-printed JSON
- Debugging support with Python's builtin logger app
- Sanity Checking module which reports on any inconsistencies it finds in the response.
- Python 3
- A desire to learn more about how the Internet works
docker pull dmuth1/dns-tool
docker run dmuth1/dns-tool google.com --text # Or pass in any other args that you want!
pip3 install git+https://github.com/dmuth/dns-tool/
usage: dns-tool [-h] [--query-type QUERY_TYPE] [--request-id REQUEST_ID]
[--json] [--json-pretty-print] [--text] [--graph] [--raw]
[--stdin] [--fake-ttl] [--debug] [--quiet]
[query] [server]
Make DNS queries and tear apart the result packets
positional arguments:
query String to query for (e.g. "google.com")
server DNS server (default: 8.8.8.8)
optional arguments:
-h, --help show this help message and exit
--query-type QUERY_TYPE
Query type (Supported types: A, AAAA, CNAME, MX, SOA,
NS) Defalt: a
--request-id REQUEST_ID
Hex value for a request ID (default: random)
--json Output response as JSON
--json-pretty-print Output response as JSON Pretty-printed
--text Output response as formatted text
--graph Output response as ASCII graph of DNS response packet
--raw Output raw DNS packet and immediately exit
--stdin Instead of making DNS query, read packet from stdin.
(works great with --raw!)
--fake-ttl Set a fake TTL, for use in test scripts where hashes
are made of the output
--debug, -d Enable debugging
--quiet, -q Quiet mode--only log errors
Let's run a query for google.com:
$ ./dns-tool google.com --text
Question
========
Question: google.com (len: 16)
Type: 1 (A (Address))
Class: 1 (IN)
Server: 8.8.8.8:53
Header
======
Request ID: 1aab
Questions: 1
Answers: 1
Authority records: 0
Additional records: 0
QR: Response
AA: Server isn't an authority
TC: Message not truncated
RD: Recursion requested
RA: Recursion available!
OPCODE: 0 - Standard query
RCODE: 0 - No errors reported
Answers
=======
Answer #0: 172.217.7.238
CLASS: 1 (IN)
TYPE: 1 (A (Address))
TTL: 154
Raw RRDATA: c0 0c 00 01 00 01 00 00 00 9a 00 04 ac d9 07 ee (len 4)
Full RRDATA: {'ip': '172.217.7.238'}
Want to see DNS packets graphed? Watch this!
./dns-tool --query-type mx gmail.com --graph
Question
========
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| QNAME: gmail.com |
| len: 5 value: gmail |
| len: 3 value: com |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| QTYPE: 15 - MX (Mail Exchange) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| QCLASS: 1 - IN |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
Header
======
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Request ID: 91dd |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode: 0 | | |RD|RA| Z: 0 | RCODE: 0 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Question Count: 1 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Answer Count: 5 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Authority/Nameserver Count: 0 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Additional Records Count: 0 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Answers
=======
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| NAME : gmail.com |
| pointer: 12 target: gmail |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| TYPE: 15 - MX (Mail Exchange) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| CLASS: 1 - IN |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| TTL: 3599 - 59 minutes from now |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| RDLENGTH: 32 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| RDDATA: 20 alt2.gmail-smtp-in.l.google.com |
| len: 4 value: alt2 |
| len: 13 value: gmail-smtp-in |
| len: 1 value: l |
| len: 6 value: google |
| pointer: 18 target: com |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| NAME : gmail.com |
| pointer: 12 target: gmail |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| TYPE: 15 - MX (Mail Exchange) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| CLASS: 1 - IN |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| TTL: 3599 - 59 minutes from now |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| RDLENGTH: 9 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| RDDATA: 40 alt4.gmail-smtp-in.l.google.com |
| len: 4 value: alt4 |
| pointer: 46 target: gmail-smtp-in |
| pointer: 18 target: com |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| NAME : gmail.com |
| pointer: 12 target: gmail |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| TYPE: 15 - MX (Mail Exchange) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| CLASS: 1 - IN |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| TTL: 3599 - 59 minutes from now |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| RDLENGTH: 4 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
| RDDATA: 5 gmail-smtp-in.l.google.com |
| pointer: 46 target: gmail-smtp-in |
| pointer: 18 target: com |
| len: 0 value: (nil) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+------------+
[snip]
This app also supports sanity checking on responses it gets from DNS servers.
If something is off (usage of a reserved field, etc.), the app will let you know.
Here is sample output from a run with --text
:
Answer
======
Answer: 172.217.15.110
QCLASS: 123 (IN)
QTYPE: 123 (A (Address))
TTL: 216
Raw RRDATA: acd90f6e (len 4)
Sanity Checks Failed
====================
Content of Z field in header is not zero: 2
Request ID on answer (aaaa) != request ID of question (beef)!
OPCODE > 2 reserved for future use! (Qtype = 14)
Invalid RCODE (77)
QCLASS in answer is < 1 (0)
This comes right from RFC 1035, section 4.1:
+---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | Resource Records (RRs) answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+
When the message is first sent out, it looks like this:
+---------------------+
| Header |
+---------------------+
| Question |
+---------------------+
The server then tweaks bits in the header accordingly and appends zero or more answers. There are a couple of neat thigs about this approach:
- 512 bytes is well under the smallest memory page size of modern CPUs
- The DNS server can malloc() 512 bytes for every request that comes in, which means that...
- The message can be modified in place--the header flags can be tweaked and answers can be appended onto the end of the question.
This might seem like a case of premature optimization, but a busy DNS resolver might handle thousands of requests per second, at which point all of the extra clock cycles that are saved really add up.
That said, the 512 byte limit is a concern, which leads us to...
Normally, the domain name(s) in an answer are in the format of a byte indicating length, followed
by that many bytes, followed by either another length byte, a zero (indicating the end of that domain name),
or a pointer. To visualize that, here's how www.google.com
would look:
0x03 www 0x06 google 0x03 com 0x00
Everybody got that? Good. Now let's talk about how pointers might fit in. If you made a
query for say, nameservers on google.com
that returned ns1.google.com
and ns2.google.com
,
you'd get an answer like this:
0x03 ns1 0x06 google 0x03 com 0x00
0x03 ns2 0xC020
Breaking that down, a pointer is two bytes, where the first two bits of the first byte are both
set to one, which is decimal 192 or hex 0xC0. So in this case, 0x20
is 32 decimal, which is the offset
for the 0x06 google
in the first answer above.
Yep. Only the last 6 bits are used.
That example I gave above wasn't quite right. You see, a pointer points to the offset from the start of the message. So there is no reason why a pointer as to point to the answer section but could instead point to the question!
Here is a truer representation of what the above answer might look like
0x03 ns1 0xC00C
0x03 ns2 0xC00C
In both cases, the answer is a length byte, "ns1" or "ns2", and a pointer to "google.com" back in the question part of that message. Yes, you can even have pointers to pointers. So if you had, for example, two additional answers of "europe.ns1.google.com" and "dk.europe.ns1.google.com", the answer might look like this:
0x03 ns1 0xCO0C
0x03 ns2 0xC00C
0x06 europe 0xC018
0x02 dk 0xC031
And just to show my work on how those offsets were calculated:
Header : 12 bytes
Question : len=6 + "google" + len=3 + "com" + 0x00 (12 bytes) + 2 bytes QTYPE + 2 bytes QCLASS = 16 bytes
Answer ns1 : len=3 + "ns1" + pointer=0xC00C + 2 bytes TYPE + 2 bytes CLASS + 4 bytes TTL + 2 bytes RDLENGTH = 16 bytes
Answer ns2 : len=3 + "ns2" + pointer=0xC00C = 5 bytes
Answer europe : len=6 + "europe" + pointer=0xC01C = 9 bytes
Answer dk : len=2 + "dk" + pointer=0xC031
Again, this seems like a bit of over-engineering, but in the context of trying to fit as much data as humanly possible into a 512 byte message, this is very efficient.
Keep in mind that the RFC for DNS was written back in 1987 -- 31 years ago as of this writing! The creators of that document were smart enough to realize that DNS would eventually become a part of the Internet's infrastructure, but they weren't sure how complex the answers might be, and needed to future-proof their procotol as best they could.
create.py
: Functions for creating the DNS requestoutput.py
: Functions for printing out the answer to a DNS queryparse.py
: Functions to parse the the headerparse_answer.py
: Functions to parse the answer headersparse_answer_body.py
: Parse the Resource Records (RR)parse_question.py
: Parse the questionsanity.py
: Functions to perform sanity checks on answer
There is a wrapper script called test.sh
which can be used to test against records
in the zone test.dmuth.org
. I have a series of test DNS records set up there that are
guaranteed to return speific results.
Running that script will produce a bunch of output like this:
Those are hashes of the output compared to what we should have gotten.
Good question! The reason I gravitated more towards this method is because I have some pretty complicated argument parsing, and I want to test different combinations of those parameters and make sure that I get the right result, hence the reason for testing full runs of the script and comparing the outputs.
To build a Docker container with dns-tool and start an interactive shell:
docker build . -t dns-tool && docker run -it -v $(pwd):/mnt --entrypoint sh dns-tool
docker tag dns-tool dmuth1/dns-tool
docker push dmuth1/dns-tool
This will create a container with Python 3 and a version of dns-tool
installed via pip.
This directory will be available in /mnt/
in case you want to run the local copy (/mnt/dns-tool
) for testing.
virtualenv virtualenv
. ./virtualenv/bin/activate
pip install -r ./requirements.txt
This will create a container with Python 3 and a version of dns-tool
installed via pip.
This directory will be available in /mnt/
in case you want to run the local copy for testing.
If you want to test out the Pip installation, the package can be installed locally with this command:
sudo pip install -e .
Things I may do at some point, depending on time, timing, and other projects:
- Handle RDNS queries/responses
- Unit testing for parsing functions
- Docker container to run a custom DNS server
- Docker containers to run different DNS servers and verify behavior across different DNS server software
- https://routley.io/tech/2017/12/28/hand-writing-dns-messages.html
- http://www.tcpipguide.com/free/t_DNSMessageHeaderandQuestionSectionFormat.htm
- https://tools.ietf.org/html/rfc1035
- Icon made by Gregor Cresnar from www.flaticon.com is licensed by CC 3.0 BY.
If you see anything wrong, or anything you'd like add, feel free to open an issue. You can also find me on Twitter and Facebook.