Reputation: 1
I've been working on a script to detect whether a given hostname is part of a delegated zone. My goal is to use this for mapping out an organization's network by identifying domain delegations.
The idea is to take a given hostname, run dig on it and see what kind of response it returns.
I put together my own approach based on how I understand delegation works, but I'd love to get some expert eyes on it! Any feedback, improvements, or suggestions would be greatly appreciated.
Here's the script: https://pastebin.com/CMKzACSm
Thanks in advance!
import subprocess
import json
import sys
import logging
# Configure logging for debugging
def run_dig(hostname):
try:
# Run dig command for NS query
result = subprocess.run(['dig', 'NS', hostname, '-4'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return result.stdout
except Exception as e:
return str(e)
def parse_dig_output(dig_output, hostname):
response = {
"details": {
"delegated": False
}
}
# Check if 'ANSWER SECTION' exists in the dig output
if "ANSWER SECTION:" in dig_output:
# Extract lines after 'ANSWER SECTION' and look for NS records
answer_section = dig_output.split("ANSWER SECTION:")[1]
ns_lines = [line.strip() for line in answer_section.splitlines() if "IN NS" in line]
# Check if any NS lines contain the hostname at the start
for line in ns_lines:
ns_host = line.split()[0]
if ns_host.startswith(hostname):
response["details"]["delegated"] = True
response["details"]["delegated_zone"] = hostname
break
# Check for rcode errors in the dig output
error_codes = ['SERVFAIL', 'NXDOMAIN', 'REFUSED', 'NXRRSET', 'NOTAUTH', 'NOTZONE']
for error_code in error_codes:
if f"rcode={error_code}" in dig_output:
response["details"]["delegated"] = True
break
return response if response["details"]["delegated"] else None
def main():
# Get the JSON input
if len(sys.argv) < 2:
sys.exit(1)
input_data = json.loads(sys.argv[1])
hostname = input_data.get("value")
if not hostname:
sys.exit(1)
# Run dig and parse the output
dig_output = run_dig(hostname)
result = parse_dig_output(dig_output, hostname)
# Output only the entries with delegated: true
if result:
print(json.dumps([result], indent=2))
else:
print("[]")
if __name__ == "__main__":
main()``
Upvotes: 0
Views: 30
Reputation: 16572
This seems more like a "Code reviews Stack Exchange" type of question, but nevertheless.
To start with, literally every DNS name except root is "part of a delegated zone". TLDs are delegated from the root in the same way that individual domains are delegated from the TLD, and in the exact same way subdomains can be further delegated by their owners.
So the query needs to be more specific: it seems you're trying either to check whether the given name is exactly at the delegation boundary (the 'zone cut' or 'start of authority'), or to find the "nearest" zone name for a given domain name.
Querying for NS is probably a good way to check whether the given name is exactly at the zone boundary if you already know the name, but if you look at the "Authority" section, you will actually find information about where the boundary is: the zone's SOA
record will be included in certain types of answers.
I think any "no such rrtype" or "no such name" response will have that, but it's easiest to query specifically for SOA records – if the name is at the top of the zone, then you'll just get its SOA in the Answer section, but in other cases you'll still get the zone's SOA in the Authority section, and either way all you need to do is look at the rrset's name.
Don't use dig to do this. This is one of those situations where using a dedicated library (such as dnspython) will actually be simpler than parsing stdout of a program:
>>> a = dns.resolver.resolve("google.com", "NS")
>>> a
<dns.resolver.Answer object at 0x7f7295cd9bd0>
>>> a.response.answer
[
<DNS google.com. IN NS RRset: [<ns2.google.com.>, <ns1.google.com.>, <ns4.google.com.>, <ns3.google.com.>]>
]
In fact, you can squeeze it down to just a single line of code, because this is a sufficiently common task that dnspython has a dedicated function to query for the SOA name:
>>> dns.resolver.zone_for_name("_imaps._tcp.gmail.com")
<DNS name gmail.com.>
If you insist on using dig, don't use the -4
option. Dig uses whatever nameservers you have in your system-wide /etc/resolv.conf file. If you have IPv6 nameservers there and they work, then there is no reason to avoid them for dig specifically.
(On the other hand, if you have IPv6 nameservers in your /etc/resolv.conf and they don't work for some reason, then why are they still in your /etc/resolv.conf in the first place?)
Similar to the earlier exception comment, your "check for rcode errors" is incorrect and only masks errors further. For example, NXDOMAIN
definitely does not tell you that the queried subdomain was delegated. If anything, it slightly implies that it wasn't.
Similarly, because you're querying your local resolver (and not an authoritative server), error codes like REFUSED
or NOTAUTH
won't occur at all; if they do occur then they are an exceptional case that tells you that your DNS setup is bad, and not merely a "let's pretend delegated=True". These specific error codes would be worth checking if you were querying the authoritative server for some domain, but right now you're not doing that.
(And NXRRSET
simply won't occur at all for regular queries; it is only ever returned for dynamic DNS updates.)
Don't do the return str(e)
thing for exceptions. It only makes it harder to check for them later. Since you did not pass check=True, no exception will be raised if dig fails anyway – meaning that any exceptions that will be raised are going to be, well, exceptional cases (like failing to spawn /usr/bin/dig
) and in those cases it's better to treat it as an error than to return made-up results.
The response
structure seems excessive. It has two fields, one of which is mostly redundant – either the function returns a structure where delegated
is always True, or it returns no response at all, so it carries no information. At most, a local (function-internal) 'is_delegated' variable would suffice.
Leaving that out, you're left with a structure that only has a single field, so you might as well simply return the delegated_zone
directly.
Upvotes: 0