def generate(self, domains: Iterable[str], filename: str): ttl = 3600 origin = "rpz.local" def read_header(): with open(filename, "r") as f: for line in f: if not line.startswith("@"): break yield line try: # Read "header" only since it's expensive to parse everything. zone = dns.zone.from_text("\n".join(read_header()), origin=origin) except (dns.exception.DNSException, IOError): zone = dns.zone.Zone(origin) existing_soa_set = zone.get_rdataset("@", "SOA") zone.nodes.clear() soa_set = zone.find_rdataset("@", "SOA", create=True) if existing_soa_set is None: soa_set.ttl = 3600 soa_set.add( dns.rdata.from_text("IN", "SOA", "@ hostmaster 0 86400 7200 2592000 86400")) logging.info("creating zone from scratch") else: soa_set.update(existing_soa_set) # Increment serial. serial = soa_set[0].serial + 1 soa_set.add(soa_set[0].replace(serial=serial)) logging.info("parsed existing header, new serial is %d", serial) in_ns = zone.find_rdataset("@", "NS", create=True) in_ns.ttl = ttl in_ns.add(dns.rdata.from_text("IN", "NS", "LOCALHOST.")) in_cname_dot = dns.rdataset.from_text("IN", "CNAME", ttl, ".") for domain in domains: zone.find_node(domain, create=True).replace_rdataset(in_cname_dot) zone.to_file(filename, sorted=True) logging.info("block list has %d domains", len(zone.nodes) - 1)
def calculate_zonemd(zone, zonemd_algorithm='sha384'): """ Calculate the digest of the zone. Returns the digest for the zone. @var zone: The zone object to digest. @type zone: dns.zone.Zone @var zonemd_algorithm: The name of the algorithm to use, either "sha384", or the number of the algorithm to use. @type zonemd_algorithm: str @raises ZoneDigestUnknownAlgorithm: zonemd_algorithm is unknown @rtype: bytes """ if zonemd_algorithm in ('sha384', ZONEMD_DIGEST_SHA384): hashing = hashlib.sha384() # Sort the names in the zone. This is needed for canonization. sorted_names = sorted(zone.keys()) # Iterate across each name in canonical order. for name in sorted_names: # Save the wire format of the name for later use. wire_name = name.canonicalize().to_wire() # Iterate across each RRSET in canonical order. sorted_rdatasets = sorted(zone.find_node(name).rdatasets, key=lambda rdataset: rdataset.rdtype) for rdataset in sorted_rdatasets: # Skip the RRSIG for ZONEMD. if rdataset.rdtype == dns.rdatatype.RRSIG: if rdataset.covers == ZONEMD_RTYPE: continue # Save the wire format of the type, class, and TTL for later use. wire_set = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass, rdataset.ttl) # Extract the wire format of the RDATA and sort them. wire_rdatas = [] for rdata in rdataset: wire_rdatas.append(rdata.to_digestable()) wire_rdatas.sort() # Finally update the digest for each RR. for wire_rr in wire_rdatas: hashing.update(wire_name) hashing.update(wire_set) hashing.update(struct.pack('!H', len(wire_rr))) hashing.update(wire_rr) return hashing.digest()
def zeroconf_to_zone(target_zone='example.com', target_ns='localhost', zeroconf_results={}, locmap={}, priomap={}, ttl=1800): import dns.name import dns.reversename import dns.resolver import dns.query from dns.exception import DNSException, Timeout from dns.resolver import NoAnswer, NXDOMAIN import dns.zone import dns.node import dns.rdataset import dns.rdata #from dns.rdatatype import * import dns.rdatatype import dns.rdataclass if not isinstance(locmap, dict): raise TypeError if not isinstance(priomap, dict): raise TypeError if target_zone == 'example.com': zone = """@ 86400 IN SOA {ns}. administrator.example.com. 1970000000 \ 28800 7200 604800 1800 @ 86400 IN NS {ns}.""".format(ns=target_ns) else: zone = dns.zone.from_xfr(dns.query.xfr(target_ns, target_zone)) zone = zone.get('@').to_text(zone.origin) zone = dns.zone.from_text(zone, origin=target_zone) # ttl = ttl if not ttl == None else \ # zone.get_rdataset('@', dns.rdatatype.SOA).ttl reverse_resolved = {} for key in zeroconf_results: inst_name, inst_type, inst_domain, inst_name_orig = key inst_subtypes = zeroconf_results[key]['subtypes'] \ if 'subtypes' in zeroconf_results[key] else [] # create service type and subtype nodes (empty nodes deleted at the # end) type_node = zone.find_node(dns.name.from_text(inst_type, origin=zone.origin), create=True) subtype_nodes = [ zone.find_node(dns.name.from_text(subtype + '._sub.' + inst_type, origin=zone.origin), create=True) for subtype in inst_subtypes ] # <Instance> must be a single DNS label, any dots should be escaped # before concatenating all portions of a Service Instance Name, # according to DNS-SD (RFC6763). # A workaround is necessary for buggy software that does not adhere to # the rules: inst_name = re.sub(r'(?<!\\)\.', r'\.', inst_name) inst_fullname = dns.name.from_text("%s.%s" % (inst_name, inst_type), origin=zone.origin) inst_addr = zeroconf_results[key]['address'] if inst_addr not in reverse_resolved: try: reverse_resolved[inst_addr] = dns.resolver.query( dns.reversename.from_address(inst_addr), dns.rdatatype.PTR) except DNSException, e: reverse_resolved[inst_addr] = None continue # inst_hostname_rev_rr = dns.resolver.query(dns.reversename. # from_address(inst_addr), # dns.rdatatype.PTR) inst_hostname_rev_rr = reverse_resolved[inst_addr] \ if reverse_resolved[inst_addr] is not None else [] zeroconf_results[key]['hostname_rev'] = [ i.to_text(relativize=False) for i in inst_hostname_rev_rr ] if not zeroconf_results[key]['hostname_rev']: continue node_ptr_rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.PTR, inst_fullname.to_text()) # fill service type and subtype nodes with PTR rdata type_node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, create=True).add(node_ptr_rdata, ttl=ttl) for subtype_node in subtype_nodes: subtype_node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, create=True).add(node_ptr_rdata, ttl=ttl) # create instance node inst_node = zone.find_node(inst_fullname, create=True) # avahi-browse returns a single SRV and TXT record so we should # try to resolve again qname = dns.name.from_text("%s.%s" % (inst_name_orig, inst_type), origin=dns.name.from_text(inst_domain)) instresolver = dns.resolver.Resolver() mdns_addr = '224.0.0.251' for l, rdt in (('srv', dns.rdatatype.SRV), ('txt', dns.rdatatype.TXT)): if inst_domain == 'local': # mDNS: # * try a multicast query first: # - with unicast-response (QU) bit set # - legacy per RFC6762 sec. 6.7 as we wont set # source_port=5353 # * fallback to direct unicast query (RFC6762 sec. 5.5) # caveat: # unicast responses must be less than 512 bytes # or else we will not get an answer (no EDNS0) a = mudns_query(qname, rdt, qwhere=mdns_addr, qport=5353, qtimeout=2, qu=True) #print "mudns returned %s" % str(a) if a is None: a = mudns_query(qname, rdt, qwhere=inst_addr, qport=5353, qtimeout=2, res=instresolver) else: a = mudns_query(qname, rdt, qtimeout=4) if isinstance(a, dns.resolver.Answer): zeroconf_results[key][l] = [rd.to_text() for rd in a[::]] else: if l == 'srv': zeroconf_results[key][l] = [ '0 0 %s %s' % (zeroconf_results[key]['port'], zeroconf_results[key]['hostname']) ] elif l == 'txt': # reverse the order of fields as returned by avahi txt_rec_rev = re.split('(?<=")\s+(?=")', zeroconf_results[key][l])[::-1] zeroconf_results[key][l] = [' '.join(txt_rec_rev)] #print "" #continue # replace hostname.local or whatever avahi returns with # reverse-resolved fqdn for h in zeroconf_results[key]['hostname_rev']: zeroconf_results[key]['srv'] = [ re.sub(r'%s\.?' % zeroconf_results[key]['hostname'], r'%s' % h, r) for r in zeroconf_results[key]['srv'] ] zeroconf_results[key]['txt'] = [ re.sub(r'%s\.?' % zeroconf_results[key]['hostname'], r'%s' % h.rstrip('.'), r) for r in zeroconf_results[key]['txt'] ] # txt record mangling for (i, txt_rec) in enumerate(zeroconf_results[key]['txt']): # Bonjour Printing mangling # discard ephemeral fields: printer-state txt_rec = txt_field_mangle(txt_rec, 'printer-state', None) zeroconf_results[key]['txt'][i] = txt_rec # note field mangling if inst_name in locmap: txt_rec = txt_field_mangle(txt_rec, 'note', locmap[inst_name]) zeroconf_results[key]['txt'][i] = txt_rec # priority field mangling if (inst_name, inst_type) in priomap: newprio = priomap[(inst_name, inst_type)] elif inst_name in priomap: newprio = priomap[inst_name] elif inst_type in priomap: newprio = priomap[inst_type] else: newprio = None if newprio is not None: prio = txt_field_mangle(txt_rec, 'priority') if prio is not None: prio = int(prio) if prio == 0: continue else: # Bonjour Printing spec v1.2 sec. 9.2.5 # if not specified, priority defaults to 50 prio = 50 # keep the modulo 10 part of original priority prio = prio % 10 txt_rec = txt_field_mangle(txt_rec, 'priority', newprio + prio) zeroconf_results[key]['txt'][i] = txt_rec # fill instance node with SRV and TXT rdata for rec_type in ('srv', 'txt'): for r in zeroconf_results[key][rec_type]: inst_node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.from_text(rec_type), create=True).add(dns.rdata.from_text( dns.rdataclass.IN, dns.rdatatype.from_text(rec_type), r), ttl=ttl)
def add_rdata(nodename: str, typ: str, ttl: int, data: str) -> None: node = zone.find_node(nodename, create=True) typ_int = dns.rdatatype.from_text(typ) rdata = dns.rdata.from_text(dns.rdataclass.IN, typ_int, data) node.find_rdataset(dns.rdataclass.IN, typ_int, create=True).add(rdata, ttl=ttl)
def zeroconf_to_zone(target_zone='example.com', target_ns='localhost', zeroconf_results={}, locmap={}, priomap={}, ttl=1800): import dns.name import dns.reversename import dns.resolver import dns.query from dns.exception import DNSException, Timeout from dns.resolver import NoAnswer, NXDOMAIN import dns.zone import dns.node import dns.rdataset import dns.rdata #from dns.rdatatype import * import dns.rdatatype import dns.rdataclass if not isinstance(locmap, dict): raise TypeError if not isinstance(priomap, dict): raise TypeError if target_zone == 'example.com': zone = """@ 86400 IN SOA {ns}. administrator.example.com. 1970000000 \ 28800 7200 604800 1800 @ 86400 IN NS {ns}.""".format(ns=target_ns) else: zone = dns.zone.from_xfr(dns.query.xfr(target_ns, target_zone)) zone = zone.get('@').to_text(zone.origin) zone = dns.zone.from_text(zone, origin=target_zone) # ttl = ttl if not ttl == None else \ # zone.get_rdataset('@', dns.rdatatype.SOA).ttl reverse_resolved = {} for key in zeroconf_results: inst_name, inst_type, inst_domain, inst_name_orig = key inst_subtypes = zeroconf_results[key]['subtypes'] \ if 'subtypes' in zeroconf_results[key] else [] # create service type and subtype nodes (empty nodes deleted at the # end) type_node = zone.find_node(dns.name.from_text(inst_type, origin=zone.origin), create=True) subtype_nodes = [zone.find_node(dns.name.from_text(subtype + '._sub.' + inst_type, origin=zone.origin), create=True) for subtype in inst_subtypes] # <Instance> must be a single DNS label, any dots should be escaped # before concatenating all portions of a Service Instance Name, # according to DNS-SD (RFC6763). # A workaround is necessary for buggy software that does not adhere to # the rules: inst_name = re.sub(r'(?<!\\)\.', r'\.', inst_name) inst_fullname = dns.name.from_text("%s.%s" % (inst_name, inst_type), origin=zone.origin) inst_addr = zeroconf_results[key]['address'] if inst_addr not in reverse_resolved: try: reverse_resolved[inst_addr] = dns.resolver.query( dns.reversename.from_address(inst_addr), dns.rdatatype.PTR) except DNSException, e: reverse_resolved[inst_addr] = None continue # inst_hostname_rev_rr = dns.resolver.query(dns.reversename. # from_address(inst_addr), # dns.rdatatype.PTR) inst_hostname_rev_rr = reverse_resolved[inst_addr] \ if reverse_resolved[inst_addr] is not None else [] zeroconf_results[key]['hostname_rev'] = [i.to_text(relativize=False) for i in inst_hostname_rev_rr] if not zeroconf_results[key]['hostname_rev']: continue node_ptr_rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.PTR, inst_fullname.to_text()) # fill service type and subtype nodes with PTR rdata type_node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, create=True).add(node_ptr_rdata, ttl=ttl) for subtype_node in subtype_nodes: subtype_node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, create=True).add(node_ptr_rdata, ttl=ttl) # create instance node inst_node = zone.find_node(inst_fullname, create=True) # avahi-browse returns a single SRV and TXT record so we should # try to resolve again qname = dns.name.from_text("%s.%s" % (inst_name_orig, inst_type), origin=dns.name.from_text(inst_domain)) instresolver = dns.resolver.Resolver() mdns_addr = '224.0.0.251' for l, rdt in (('srv', dns.rdatatype.SRV), ('txt', dns.rdatatype.TXT)): if inst_domain == 'local': # mDNS: # * try a multicast query first: # - with unicast-response (QU) bit set # - legacy per RFC6762 sec. 6.7 as we wont set # source_port=5353 # * fallback to direct unicast query (RFC6762 sec. 5.5) # caveat: # unicast responses must be less than 512 bytes # or else we will not get an answer (no EDNS0) a = mudns_query(qname, rdt, qwhere=mdns_addr, qport=5353, qtimeout=2, qu=True) #print "mudns returned %s" % str(a) if a is None: a = mudns_query(qname, rdt, qwhere=inst_addr, qport=5353, qtimeout=2, res=instresolver) else: a = mudns_query(qname, rdt, qtimeout=4) if isinstance(a, dns.resolver.Answer): zeroconf_results[key][l] = [rd.to_text() for rd in a[::]] else: if l == 'srv': zeroconf_results[key][l] = [ '0 0 %s %s' % (zeroconf_results[key]['port'], zeroconf_results[key]['hostname']) ] elif l == 'txt': # reverse the order of fields as returned by avahi txt_rec_rev = re.split('(?<=")\s+(?=")', zeroconf_results[key][l] )[::-1] zeroconf_results[key][l] = [' '.join(txt_rec_rev)] #print "" #continue # replace hostname.local or whatever avahi returns with # reverse-resolved fqdn for h in zeroconf_results[key]['hostname_rev']: zeroconf_results[key]['srv'] = [ re.sub( r'%s\.?' % zeroconf_results[key]['hostname'], r'%s' % h, r) for r in zeroconf_results[key]['srv'] ] zeroconf_results[key]['txt'] = [ re.sub( r'%s\.?' % zeroconf_results[key]['hostname'], r'%s' % h.rstrip('.'), r) for r in zeroconf_results[key]['txt'] ] # txt record mangling for (i, txt_rec) in enumerate(zeroconf_results[key]['txt']): # Bonjour Printing mangling # discard ephemeral fields: printer-state txt_rec = txt_field_mangle(txt_rec, 'printer-state', None) zeroconf_results[key]['txt'][i] = txt_rec # note field mangling if inst_name in locmap: txt_rec = txt_field_mangle(txt_rec, 'note', locmap[inst_name]) zeroconf_results[key]['txt'][i] = txt_rec # priority field mangling if (inst_name, inst_type) in priomap: newprio = priomap[(inst_name, inst_type)] elif inst_name in priomap: newprio = priomap[inst_name] elif inst_type in priomap: newprio = priomap[inst_type] else: newprio = None if newprio is not None: prio = txt_field_mangle(txt_rec, 'priority') if prio is not None: prio = int(prio) if prio == 0: continue else: # Bonjour Printing spec v1.2 sec. 9.2.5 # if not specified, priority defaults to 50 prio = 50 # keep the modulo 10 part of original priority prio = prio % 10 txt_rec = txt_field_mangle(txt_rec, 'priority', newprio + prio) zeroconf_results[key]['txt'][i] = txt_rec # fill instance node with SRV and TXT rdata for rec_type in ('srv', 'txt'): for r in zeroconf_results[key][rec_type]: inst_node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.from_text(rec_type), create=True).add( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.from_text(rec_type), r), ttl=ttl)