Scapy – DNS queries with EDNS

To debug some issues with DNS (specifically EDNS related issues) I thought I would use Scapy so that I could craft the packets the exact way I wanted. As the issue was only occurring for some queries but not others due to the queries being sent to different front end servers I had to run multiple queries.

For the complete source, skip to the end of the post.

First to get started I will import the required modules and set the variables for things like number of queries to send, the time out for queries, the host name to send queries and other query related parameters.

#!/usr/bin/env -S python3 -O

"""
Send queries with/without EDNS options and check response codes

Requires Python 3.6 or higher and scapy.
"""

from scapy.all import DNS, DNSQR, DNSRROPT, IP, sr1, UDP, RandShort
from typing import Dict

## Hosts to send queries to
hosts: Dict[str, str] = {
    "LOCAL": "192.168.1.1",
    "REMOTE": "192.168.2.1",
}

## Number of queries to send
number: int = 20

## Timeout
timeout: int = 3

## Query name
qname: str = "the.hostname.i.want.to.query"

## Query type
qtype: str = "A"

## Possible response codes
rcodes: Dict[int, str] = {
    0: "ok",
    1: "format-error",
    2: "server-failure",
    3: "name-error",
    4: "not-implemented",
    5: "refused",
}

## If running in debug mode raise Scapy verbosity
if __debug__:
    verbosity: int = 1
else:
    verbosity: int = 0

Next, I will create a helper function to actually send off the queries multiple times:

def send_queries(query: object) -> None:
    """Send queries and output results
    Arguments:
        query (object): The query to send
    """
    ## Loop X times
    for _ in range(number):
        ## Send query
        res = sr1(query, timeout=timeout, verbose=verbosity)
        ## Check for answer
        if res:
            ## Get response code name
            rname: str = rcodes[res[DNS].rcode]
            print(f"\t- Response code: {rname}")
        else:
            print(f"\t- No DNS response")

And now I can move on to building the various test cases.

First I just want to send a standard query with no special options set:

"""
Test case 1: Query with no EDNS options set
"""
print(
    "===========================================================\n"
    "Test case 1: Query with no EDNS options set\n"
    "==========================================================="
)
for host, ip in hosts.items():
    print(f"Generating queries to host {host} ({ip}):")
    ## Generate query to send
    query = (
        IP(dst=ip)
        / UDP(sport=RandShort(), dport=53)
        / DNS(qd=DNSQR(qname=qname, qtype=qtype, qclass="IN"))
    )
    ## Send queries
    send_queries(query=query)

Now I can create test case 2, this time with a 512 byte EDNS buffer size set:

"""
Test case 2: Query with EDNS buffer size of 512 bytes
"""
print(
    "===========================================================\n"
    "Test case 2: Query with 512 byte EDNS buffer size\n"
    "==========================================================="
)
for host, ip in hosts.items():
    print(f"Generating queries to host {host} ({ip}):")
    ## Generate query to send
    query = (
        IP(dst=ip)
        / UDP(sport=RandShort(), dport=53)
        / DNS(qd=DNSQR(qname=qname, qtype=qtype, qclass="IN"), ar=DNSRROPT(rclass=512))
    )
    ## Send queries
    send_queries(query=query)

And another test case this time with a 4096 byte buffer size:

"""
Test case 3: Query with EDNS buffer size of 4096 bytes
"""
print(
    "===========================================================\n"
    "Test case 3: Query with 4096 byte EDNS buffer size\n"
    "==========================================================="
)
for host, ip in hosts.items():
    print(f"Generating queries to host {host} ({ip}):")
    ## Generate query to send
    query = (
        IP(dst=ip)
        / UDP(sport=RandShort(), dport=53)
        / DNS(qd=DNSQR(qname=qname, qtype=qtype, qclass="IN"), ar=DNSRROPT(rclass=4096))
    )
    ## Send queries
    send_queries(query=query)

For good measure, I will add test cases with for the above queries that generate packets with the “DF” (do not fragment) bit set:

"""
Test case 4: Query with no EDNS options set and DF bit set
"""
print(
    "===========================================================\n"
    "Test case 4: Query with no EDNS options set and DF bit set\n"
    "==========================================================="
)
for host, ip in hosts.items():
    print(f"Generating queries to host {host} ({ip}):")

    ## Generate query to send
    query = (
        IP(dst=ip,flags='DF')
        / UDP(sport=RandShort(), dport=53)
        / DNS(qd=DNSQR(qname=qname, qtype=qtype, qclass="IN"))
    )

    ## Send queries
    send_queries(query=query)

"""
Test case 5: Query with EDNS buffer size of 512 bytes and DF bit set
"""
print(
    "===========================================================\n"
    "Test case 5: Query with 512 byte EDNS buffer size and DF bit set\n"
    "==========================================================="
)
for host, ip in hosts.items():
    print(f"Generating queries to host {host} ({ip}):")

    ## Generate query to send
    query = (
        IP(dst=ip,flags='DF')
        / UDP(sport=RandShort(), dport=53)
        / DNS(qd=DNSQR(qname=qname, qtype=qtype, qclass="IN"), ar=DNSRROPT(rclass=512))
    )

    ## Send queries
    send_queries(query=query)

"""
Test case 6: Query with EDNS buffer size of 4096 bytes and DF bit set
"""
print(
    "===========================================================\n"
    "Test case 6: Query with 4096 byte EDNS buffer size and DF bit set\n"
    "==========================================================="
)
for host, ip in hosts.items():
    print(f"Generating queries to host {host} ({ip}):")

    ## Generate query to send
    query = (
        IP(dst=ip,flags='DF')
        / UDP(sport=RandShort(), dport=53)
        / DNS(qd=DNSQR(qname=qname, qtype=qtype, qclass="IN"), ar=DNSRROPT(rclass=4096))
    )

    ## Send queries
    send_queries(query=query)

After running the test cases I get the results I was looking for; an easily reproducible test to provide to the administrator of the name servers so that they can check and fix their configuration.

Leave a Reply

Your email address will not be published. Required fields are marked *