Reputation: 485
I'm looking to query a domain like this:
dns.resolver.resolve("dnssec-failed.org","A")
Which returns an error like this:
raise NoNameservers(request=self.request, errors=self.errors)
dns.resolver.NoNameservers: All nameservers failed to answer the query dnssec-failed.org. IN A: Server 127.0.0.1 UDP port 53 answered SERVFAIL
I want to be able to catch that exception in my function like so:
def get_a_record(url):
try:
answers = dns.resolver.resolve(url,"A")
except dns.resolver.SERVFAIL:
print("SERVFAIL error for %s" % url)
except dns.resolver.NXDOMAIN:
print("No such domain %s" % url)
except dns.resolver.Timeout:
print("Timed out while resolving %s" % url)
except dns.exception.DNSException:
print("Unhandled exception")
Now I know in the above snippet dns resolver doesn't have a SERVAIL exception but what I'd like to do is catch the error, be able to log it, and continue my script. Is there a proper way to do this using the dns resolver package, or would I need to call the dig
command and parse that result?
EDIT
For clarification, I only used dnssec-failed.org as an example because it results in (what I thought) would be the same response as something I am specifically looking, for but don't actually have any active examples of. That "something" being domains which point to ip addresses that are no longer in use. Dangling NS records in other words.
For example I use an IP address that is loaned to me by AWS for use in some XYZ cloud-based application, and I create the name-to-address mapping records in my DNS zone. If I decide to deprecate this service and return the ip back to the cloud provider's pool of ips but forget to remove the DNS record from the zone, it is left "dangling".
That is what I am looking for and I mistakenly assumed that a SERVFAIL is the type of response I get from a query like dig domain-with-no-ip.com
Apologies for the confusion.
EDIT 2
I went and tested this by taking a domain I'd already registered. Configured an A record for it and pointed it to an Ubuntu EC2 listening on port 7272 (python3 -m http.server 7272). Waited 5 minutes for the zone to propagate and then I was able to reach my domain, publicly. All fine and good.
Then I stopped the instance, waited a bit, and then restarted it. Upon coming back up it had a new public ip. Great. So at this point there is a dangling A record for me to test.
So I do dig and nslookup on the domain. Both come back with perfectly fine answers. They just simply point to the now old/original public ip. And that makes sense since the DNS record hasn't changed. The only observable thing that really changes is something like curl, which times out.
So unless my understanding is still wrong, there really isn't an all-too reliable way to hunt down dangling A records because basing logic off n http timeout doesn't necessarily imply a dangling record. The server could just be off/down and the ip is still attached to the resource. Am I correct in my understanding or am I missing something still?
EDIT 3
Accepting the answer because even though my question mildly evolved into something else, the answer did technically address the original question of my post and I think that warrants accepting it.
Upvotes: 0
Views: 2738
Reputation: 12505
First, dnssec-failed.org
has nameservers but is, by design, failing DNSSEC.
Hence a simple query towards any recursive nameserver that does DNSSEC validation will fail with SERVFAIL
as expected:
$ dig dnssec-failed.org NS +short
(no output)
$ dig dnssec-failed.org NS | grep status: | tail -1
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 46517
but as soon as you disable DNSSEC validation you get the nameservers as it should be:
$ dig dnssec-failed.org NS +cdflag +noall +ans +nottlunits
dnssec-failed.org. 7200 IN NS dns105.comcast.net.
dnssec-failed.org. 7200 IN NS dns101.comcast.net.
dnssec-failed.org. 7200 IN NS dns102.comcast.net.
dnssec-failed.org. 7200 IN NS dns103.comcast.net.
dnssec-failed.org. 7200 IN NS dns104.comcast.net.
Now back to the Python part.
resolve()
from dnspython is an high level API call, it does everything a resolver does, that is it potentially recurse from root up to being able to give you an answer. Hence, this simple call hides possibly multiple questions and responses and as such may not expose you to the real underlying problem, but provides high level API in output also, using an exception.
As you can see in your own example, you have the SERVFAIL
right in the error message, but it is an NoNameservers
exceptions because the code asked the registry nameservers for the list of nameservers (which works, there is a DS for this name in parent nameservers), and then ask for any of those nameservers for further data and then they fail there DNSSEC validation, hence the final exception.
It is not clear to me what is your position on DNSSEC error in your case, if you do not care about them or if you really want to study them and do something particular. Hence the above solutions may need to be adapted. If you do not care, just log the NoNameservers
exception and go on, everything will work as excepted, DNSSEC validation error will happen exactly like a broken domain, which is per design.
Hence do you really need to handle DNSSEC errors in any way different from any other errors? Why can't you catch NoNameservers
exception, log it, and go further?
Otherwise the quick (and dirty way), just parse the error message attached to the NoNameservers
exception, and if you see SERVFAIL
you can suppose (but not be 100% sure) it is a DNSSEC problem, and at least go further as you need.
If you really need to have further details and be sure it is a DNSSEC problem, you need to do the equivalent of what is above for dig
, that is do 2 queries that just differ in the CD
DNS flag, and compare results. Which means going "lower" than resolve()
API and use dns.query
directly, such as this way:
>>> import dns, dns.rcode
>>> resolver_ip = '8.8.8.8' # Use any recursive **validating** nameserver that you trust
>>> query=dns.message.make_query('dnssec-failed.org', 'A')
>>> response = dns.query.udp_with_fallback(query, resolver_ip)[0]
>>> response.rcode() == dns.rcode.SERVFAIL
True
# Now checking if disabling DNSSEC resolves the problem and gets us a reply
# If so, it really means there is a DNSSEC problem
>>> print(str(query))
id 65008
opcode QUERY
rcode NOERROR
flags RD
;QUESTION
dnssec-failed.org. IN A
;ANSWER
;AUTHORITY
;ADDITIONAL
>>> query.flags
<Flag.RD: 256>
>>> query.flags = query.flags | dns.flags.CD
>>> query.flags
<Flag.RD|CD: 272>
>>> print(str(query))
id 65008
opcode QUERY
rcode NOERROR
flags RD CD
;QUESTION
dnssec-failed.org. IN A
;ANSWER
;AUTHORITY
;ADDITIONAL
# We enabled flag "CD" aka checking disabled aka please do not do any DNSSEC validation, and now doing the same query as above again:
>>> response = dns.query.udp_with_fallback(query, resolver_ip)[0]
>>> response.rcode() == dns.rcode.SERVFAIL
False
>>> response.rcode() == dns.rcode.NOERROR
True
>>> response.answer[0][0]
<DNS IN A rdata: 69.252.80.75>
Upvotes: 1