def record_reader(self, zone_file): with open(zone_file, 'r') as root_hints_file: for line in root_hints_file: if not line or line[0] == ';': continue tokens = re.split(r'\s*\s', line.rstrip()) if len(tokens) == 5: (name, ttl, rclass, rtype, rdata) = tokens rclass = DnsRClass[rclass] elif len(tokens) == 4: (name, ttl, rtype, rdata) = tokens rclass = DnsRClass.IN # lets just assume else: # Don't know what this is continue try: rtype = DnsRType[rtype] ttl = int(ttl) except ValueError: self.log.critical('Malformed glue records.') continue name = DomainName.from_string(name) if rtype == DnsRType.NS: record = DnsRecord(name, rtype, rclass, ttl, rdata=DomainName.from_string(rdata).encode()) elif rtype == DnsRType.A: record = DnsRecord(name, rtype, rclass, ttl, rdata=ipaddress.IPv4Address(rdata).packed) elif rtype == DnsRType.AAAA: record = DnsRecord(name, rtype, rclass, ttl, rdata=ipaddress.IPv6Address(rdata).packed) self.log.debug('Glue record: %s' % record) yield record
def get_name(self, name_id): with closing(self.db.cursor()) as cursor: sequence = [] for foo in range(256): # dns labels are limited to 256 depths anyway cursor.execute('SELECT parent,name FROM name WHERE `id`=%s LIMIT 1', (name_id,)) self.queries += 1 (name_id, label) = cursor.fetchone() sequence.append(label) if name_id is None: return DomainName(sequence)
def enumerate_nameserver_addresses(nameserver_records, additional_records): nameserver_records = list(nameserver_records) additional_records = list(additional_records) random.shuffle(nameserver_records) random.shuffle(additional_records) # TODO: consider moving away from ns,addr pairs, and just using addr soley for nameserver_record in nameserver_records: assert isinstance(nameserver_record, DnsRecord) if nameserver_record.rtype == DnsRType.NS: # TODO: support domain name label compression for NS rdata ns_name = DomainName.parse(nameserver_record.rdata) for address_record in additional_records: assert isinstance(address_record, DnsRecord) if address_record.name == ns_name and address_record.rtype in [DnsRType.A]: yield (nameserver_record, address_record)
def resolve(self, dns_packet): assert isinstance(dns_packet, DnsPacket) assert hasattr(dns_packet, 'pk') # TODO: consider http://tools.ietf.org/html/draft-ietf-dnsext-edns1-03 # TODO: try/except to set proper RCODE assert dns_packet.QDCOUNT > 0 if dns_packet.QDCOUNT > 1: assert all(map(lambda question: question.name == dns_packet.questions[0].name, dns_packet.questions[1:])) assert all( map(lambda question: question.qclass == dns_packet.questions[0].qclass, dns_packet.questions[1:])) response = self.db.lookup_response(dns_packet.questions) if response: return response # TODO: deduplicate at the packet level? self.db.create_query(dns_packet.pk) # TODO: if not zone_cut bootstrap() ? root_hints = self.db.lookup_response(DnsQuestion(root_label, DnsQType.NS)) next_zone_cut = root_hints # Determine SOA for zone in dns_packet.questions[0].name.enumerate_hierarchy(): # TODO: opportunity here for async operation question = DnsQuestion(zone, DnsQType.SOA) zone_soa = self.db.lookup_response(question) #next_zone_cut = root_hints ??? while next_zone_cut and not zone_soa: assert isinstance(next_zone_cut, Response) zone_cut = next_zone_cut next_zone_cut = None for (nameserver, additional_record) in self.enumerate_nameserver_addresses(zone_cut.nameservers,zone_cut.additional_records): # TODO: Use/Check resolved AA queries from cached records, from zone_cut # As fallback, use glue # TODO: Nameserver(nameserver_record)? ns_name = DomainName.parse(nameserver.rdata) try: response = yield from self.query([question], nameserver, additional_record,parent_id=dns_packet.pk) assert isinstance(response, Response) except asyncio.CancelledError: pass except asyncio.InvalidStateError: pass except AssertionError: pass else: self.log.debug('resolve() %s' % response) if response.RCODE == DnsResponseCode.no_error: # TODO: and rtype == SOA etc if response.ANCOUNT == 0: if response.NSCOUNT > 0: # TODO: lame delegation # TODO: if RD # TODO: when we ask com for example.com our zone=example.com, when zone_cut is a.iana-servers.net, should attempt to resolve the address of the NS first, then rely on glue... this will require another recursion # for _ns in response.nameservers: # #TODO: ask for A or AAAA based on the ipv4/6 of socket # #TODO: check for a cached record, if one is not found, attempt to resolve it, if that fails, use glue # #cached_response = yield from self.resolve(Query(questions=[DnsQuestion(DomainName.parse(_ns.rdata), DnsQType.A)])) # # if cached_response: # # next_zone_cut = cached_response # # break # We found a cached address for the ns, break out so we can use this as next zone cut # # TODO: this whole thing is a mess, and needs rewritting... the break here will prevent continuing in the event that the address we have cached is non responsive... a stack type mechanism would be much more suitable, then recursion depth could be controlled in a sane fashion # else: # Use the glue next_zone_cut = response break elif response.ANCOUNT >= 1: # TODO: ANCOUNT == 1 probably shouldn't be a requirement next_zone_cut = zone_cut # Reuse the zone_cut that produced this authorative response zone_soa = RData_SOA.parse(response.answers[0].rdata) if ns_name != zone_soa.mname: pass if response.questions[0].name == dns_packet.questions[0].name: result = yield from self.query(dns_packet.questions, nameserver, additional_record, parent_id=dns_packet.pk) if isinstance(result, Response): result.ID = dns_packet.ID return result # We received an authorative SOA response, but not from the preferred NS # TODO: consider using zone_soa as parent_id? # TODO: enforce expires/refresh/minimum TTL values # TODO: support negative caching (if response.AA) else: pass # zone_cut = response break # TODO: parallel requests, this line skips the rest of the ns,addr pairs if not response: pass