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.