#!/usr/bin/env python3

"""Query all the IP addresses of all the authoritative name servers
of a domain to check CDS/CDNSKEY/CSYNC consistency, as mandated by
https://datatracker.ietf.org/doc/draft-ietf-dnsop-cds-consistency/"""

"""Usage: csd-consistency domain [type]

Where "type" is CDS (the default), CDNSKEY or CSYNC.

If the name servers are consistent, return code is 0. If they are not,
we return 1. If there is a problem such as a non-responding server,
return code is 2."""

# You must install DNSpython <https://www.dnspython.org/>
import dns.message
import dns.query
import dns.resolver
import dns.rdatatype

# Python Standard Library
import sys

timeout = 1

def usage():
    print("Usage: %s domain [type]" % sys.argv[0], file=sys.stderr)

def get_ns(domain):
    result = []
    try:
        answers = dns.resolver.resolve(domain, "NS")
    except dns.resolver.NXDOMAIN:
        print("%s does not exist" % domain, file=sys.stderr)
        sys.exit(2)
    except dns.resolver.NoNameservers:
        print("%s has no working nameservers" % domain, file=sys.stderr)
        sys.exit(2)
    except dns.resolver.NoAnswer:
        print("%s is not a zone (no NS records)" % domain, file=sys.stderr)
        sys.exit(2)        
    for rdata in answers:
        if rdata.rdtype == dns.rdatatype.NS:
           result.append(rdata.target)
    return result

def addresses_of(server):
    result = []
    try:
        answers = dns.resolver.resolve(server, "A")
        for rdata in answers:
            if rdata.rdtype == dns.rdatatype.A:
                result.append((rdata.address, str(server)))
    except dns.resolver.NoAnswer:
        pass
    try:
        answers = dns.resolver.resolve(server, "AAAA")
        for rdata in answers:
            if rdata.rdtype == dns.rdatatype.AAAA:
                result.append((rdata.address, str(server)))
    except dns.resolver.NoAnswer:
        pass
    return result

if len(sys.argv) < 2 or len(sys.argv) > 3:
    usage()
    sys.exit(3)
name = sys.argv[1]
if len(sys.argv) == 3:
    tipe = sys.argv[2].upper()
    if tipe == "CDS":
        pass
    elif tipe == "CDNSKEY":
        pass
    elif tipe == "CSYNC":
        print("Warning, we do not implement all the tests for CSYNC", file=sys.stderr)
    else:
        print("Record type %s not supported" % (tipe),
              file=sys.stderr)
        sys.exit(3)        
    tipe = dns.rdatatype.from_text(tipe)
else:
    tipe = dns.rdatatype.CDS

auths = get_ns(name)
addrs = []
for ns in auths:
    for address in addresses_of(ns):
        addrs.append(address) 

data = None
first = True
for server_ip, server_name in addrs:
    message = dns.message.make_query(name, tipe, use_edns=True,
                                     want_dnssec=False)
    try:
        response = dns.query.udp(message, server_ip, timeout=timeout)
    except dns.exception.Timeout:
        print("%s: Name server %s/%s does not reply" % (name, server_name, server_ip),
              file=sys.stderr)
        sys.exit(2)
    for rrset in response.answer:
        for record in rrset:
            if record.rdtype == tipe:
                if not first:
                    if str(record) == data: # TODO for CSYNC, the test
                        # must be a bit more subtle, see
                        # draft-ietf-dnsop-cds-consistency, section
                        # 3.2.
                        print("Inconsistency in %s at %s/%s, previous was \"%s\"" % \
                              (name, server_name, server_ip, data),
                              file=sys.stderr)
                        sys.exit(1)
                else:
                    first = False
                    data = record
print("%s is consistent, data is \"%s\"" % (name, data))
sys.exit(0)
