Problem 7.5

The dig tool

The dig command is a popular tool for querying DNS name servers. You task is to expose the dig command in Python as a function dig.

The dig command

Let's look at the output of the dig command to understand how it works.

$ dig amazon.com

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> amazon.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49697
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;amazon.com.			IN	A

;; ANSWER SECTION:
amazon.com.		128	IN	A	205.251.242.103
amazon.com.		128	IN	A	54.239.28.85
amazon.com.		128	IN	A	52.94.236.248

;; Query time: 16 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Wed Jan 18 13:45:30 IST 2023
;; MSG SIZE  rcvd: 87

The output is too verbose. We can silence everything else and show only answer using:

$ dig +noall +answer amazon.com
amazon.com.		128	IN	A	205.251.242.103
amazon.com.		128	IN	A	54.239.28.85
amazon.com.		128	IN	A	52.94.236.248

We used the dig command to query the name server to look up for amazon.com. By default, it queries for records of type A, which stands for IPv4 Address.

The DNS server responded with three records matching the domain name amazon.com. Each entry contains 5 fields, namely the domain name, TTL, class, record type and content.

See how to read dig output on wizard zines for more details.

Advanced Usage

Record Type

We can optionally pass the record type to dig to query of other types of DNS records like MX (mail exchange), TXT (text notes), NS (name server) etc.

The following command, queries for records of type MX.

$ dig +noall +answer amazon.com mx
amazon.com.		766	IN	MX	5 amazon-smtp.amazon.com.

In case of MX records, the content contains the priority and the mail server name. There could be more than one entry.

The following command queries for records of type NS.

$ dig +noall +answer amazon.com ns
amazon.com.		1626	IN	NS	pdns6.ultradns.co.uk.
amazon.com.		1626	IN	NS	ns3.p31.dynect.net.
amazon.com.		1626	IN	NS	ns4.p31.dynect.net.
amazon.com.		1626	IN	NS	ns1.p31.dynect.net.
amazon.com.		1626	IN	NS	ns2.p31.dynect.net.
amazon.com.		1626	IN	NS	pdns1.ultradns.net.

Server

By default, the dig command queries the DNS server configured in the system. However, we can explicitly pass a server to query it.

$ dig +noall +answer amazon.com @8.8.8.8
amazon.com.		128	IN	A	205.251.242.103
amazon.com.		128	IN	A	54.239.28.85
amazon.com.		128	IN	A	52.94.236.248

The 8.8.8.8 is the Google DNS server. The output would look exactly the same in the normal times, but it is very handy to troubleshoot DNS issues with your domain by querying various known DNS server.

There are some hobby DNS servers like dns.toys that provide useful utilities over dns.

$ dig +noall +answer mumbai.weather @dns.toys
mumbai.			1	IN	TXT	"Mumbai (IN)" "30.40C (86.72F)" "32.10% hu." "clearsky_day" "14:30, Wed"
mumbai.			1	IN	TXT	"Mumbai (IN)" "27.90C (82.22F)" "42.80% hu." "clearsky_day" "16:30, Wed"
mumbai.			1	IN	TXT	"Mumbai (IN)" "23.00C (73.40F)" "71.40% hu." "clearsky_night" "18:30, Wed"
mumbai.			1	IN	TXT	"Mumbai (IN)" "20.40C (68.72F)" "90.50% hu." "clearsky_night" "20:30, Wed"
mumbai.			1	IN	TXT	"Mumbai (IN)" "18.80C (65.84F)" "90.40% hu." "clearsky_night" "22:30, Wed"

Summary of Usage

The dig command is used as follows:

dig options domain-name record-type @server

The options, record-type and server are optional.

The Python API

You task is to implement a Python function dig that calls the dig command with appropriate arguments, parse the output and returns the result as a Python data structure.

The funtion takes the doman name as argument and two optional arguments, record_type and server.

Sample Usage:

>>> dig("amazon.com")
[
    {"name": "amazon.com.", "ttl": 128, "class": "IN", "record_type": "A", "content": "205.251.242.103"},
    {"name": "amazon.com.", "ttl": 128, "class": "IN", "record_type": "A", "content": "54.239.28.85"},
    {"name": "amazon.com.", "ttl": 128, "class": "IN", "record_type": "A", "content": "52.94.236.248"}
]

>>> dig("amazon.com", record_type="MX")
[
  {"name": "amazon.com", "ttl": 498, "class": "IN", "record_type": "MX", "content": "5 amazon-smtp.amazon.com."}
]

>>> dig("amazon.com", record_type="MX", server="8.8.8.8")
[
  {"name": "amazon.com", "ttl": 498, "class": "IN", "record_type": "MX", "content": "5 amazon-smtp.amazon.com."}
]

>>> dig("mumbai.weather", server="dns.toys")
[
    {"name": "mumbai.", "ttl": 1, "class": "IN", "record_type": "TXT", "content": '"Mumbai (IN)" "30.40C (86.72F)" "32.10% hu." "clearsky_day" "14:30, Wed"'},
    ...
]

Solution

import subprocess

def parse_record(line):
    name, ttl, klass, record_type, content = line.strip().split(None, 4)
    return {
        "name": name,
        "ttl": int(ttl),
        "record_type": record_type,
        "class": klass,
        "content": content
    }

def dig(name, record_type="A", server=None):
    """Python interface to the dig command.
    """
    cmd = ["dig", '+noall', '+answer', name, record_type]
    if server:
        cmd.append(f"@{server}")
    print(cmd)
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True)
    return [parse_record(line) for line in p.stdout]