def _write_resp(self, dgram, rtime, ttype, csv): if csv: print >>csv, '%s,%s,%s,%s,%s,%.3f,%s,%d,%d,%s' % (self.service.name, self.address, \ str(dgram.question[0].name), dt.to_text(dgram.question[0].rdtype), ttype, \ rtime * 1000, dc.to_text(dgram.rcode()), len(dgram.answer), \ dgram.answer[0].ttl if len(dgram.answer) > 0 else 0, \ '|'.join([str(rd) for rd in self._get_rdata(dgram)]))
def TestCDNAnswers(self, record_type, record, timeout=None): """Test to see that an answer returns correct IP's. Args: record_type: text record type for NS query (A, CNAME, etc) record: string to query for timeout: timeout for query in seconds (int) Returns: (is_broken, error_msg, duration) """ is_broken = False error_msg = '' duration = -1 host = '' if not timeout: timeout = self.health_timeout (response, duration, error_msg) = self.TimedRequest(record_type, record, timeout) if response and not rcode.to_text(response.rcode()) in FATAL_RCODES and response.answer: for answer in response.answer: for rdata in answer: if rdata.rdtype == 1: host = str(rdata.address) # ip, time_min, time_avg, time_max, lost duration = shell_ping.ping(host, times=5)[1] # the min ping time among the results break if duration == -1: is_broken = True error_msg = '%s is failed to resolve' % record.rstrip('.') else: error_msg = '%dms ping time to CDN host %s(%s)' % (duration, record.rstrip('.'), host) return (is_broken, error_msg, duration)
def norecurse_cache_snoop(name, server, out=False): try: response = return_response_norecurse(name, server) except exception.Timeout: if out: print("The query timed out") return False if response.rcode() == 0: if out: if len(response.answer) == 0: print("The hostname {} was NOT cached.".format(name)) else: print("The hostname {} was cached!".format(name)) return True else: if out: print("The server had an issue with the non-recursive query...") print("Response code: {}\n".format(rcode.to_text( response.rcode()))) return False
def parse_query(query, nameserver, duration): """ Parse a dns response into a dict based on record type. Should adhere to propsed rfc format: http://tools.ietf.org/html/draft-bortzmeyer-dns-json-00 """ flag_list = flags.to_text(query.response.flags) return { 'Query': get_query(nameserver, duration), 'QuestionSection': get_question(query), 'AnswerSection': get_rrs_from_rrsets(query.response.answer), 'AdditionalSection': get_rrs_from_rrsets(query.response.additional), 'AuthoritySection': get_rrs_from_rrsets(query.response.authority), 'ReturnCode': rcode.to_text(query.response.rcode()), 'ID': query.response.id, 'AA': 'AA' in flag_list, 'TC': 'TC' in flag_list, 'RD': 'RD' in flag_list, 'RA': 'RA' in flag_list, 'AD': 'AD' in flag_list }
def TestRootNsResponse(self): """Test a . NS response. NOTE: This is a bad way to gauge performance of a nameserver, as the response length varies between nameserver configurations. """ is_broken = False error_msg = None (response, duration, error_msg) = self.TimedRequest('NS', '.') if not response: response_code = None is_broken = True if not error_msg: error_msg = 'No response' else: response_code = rcode.to_text(response.rcode()) if response_code in FATAL_RCODES: error_msg = response_code is_broken = True return (is_broken, error_msg, duration)
def dnsck_query( dns_server: str, dns_query: str, record_type: str, iterations: int, tcp: bool = False, nosleep: bool = False, ) -> int: """Perform a DNS query for a set number of iterations. Args: dns_server (str): IP address of server. dns_query (str): Query to lookup. record_type (str): Record type. iterations (int): Number of iterations. tcp (bool): Use TCP for query. nosleep (bool): Disable sleep. Returns: int: Number of errors. """ result_code_dict: DefaultDict[str, int] = defaultdict(int) query_times = [] # type: List[float] record_number = 0 # type: int response_errors = 0 # type: int iteration_count = 0 # type: int try: make_dns_query = message.make_query(dns_query, record_type.upper(), use_edns=True) except rdatatype.UnknownRdatatype: print("Unknown record type, try again.") sys.exit(1) print( f"Performing {iterations} queries to server {dns_server} for domain {dns_query}", f"with record type {record_type.upper()}.\n", ) try: for iteration in range(iterations): print(f"[Query {iteration + 1} of {iterations}]") try: if tcp: dns_response = query.tcp(make_dns_query, dns_server, timeout=10) else: dns_response = query.udp(make_dns_query, dns_server, timeout=10) if dns_response.answer: for answer in dns_response.answer: print(answer) record_number = len(answer) else: print("No records returned.") elapsed_time = dns_response.time * 1000 # type: float if elapsed_time < 500: result_code = rcode.to_text( dns_response.rcode()) # type: str result_code_dict[result_code] += 1 iteration_count += 1 else: result_code = "Degraded" result_code_dict[result_code] += 1 iteration_count += 1 response_errors += 1 except exception.Timeout: print("Query timeout.") result_code = "Timeout" result_code_dict[result_code] += 1 elapsed_time = 10000 iteration_count += 1 response_errors += 1 if not nosleep: time.sleep(1) query_times.append(elapsed_time) print(f"Records returned: {record_number}") print(f"Response time: {elapsed_time:.2f} ms") print(f"Response status: {result_code}\n") except KeyboardInterrupt: print("Program terminating...") print("Response status breakdown:") for query_rcode, count in result_code_dict.items(): print(f"{count} {query_rcode}") print( f"\nSummary: Performed {iteration_count} queries to server {dns_server}", f"for domain {dns_query} with record type {record_type.upper()}.", f"\nResponse errors: {response_errors / iteration_count * 100:.2f}%", ) print( f"Average response time: {sum(query_times) / len(query_times):.2f} ms\n" ) return response_errors
def TestAnswers(self, record_type, record, expected, critical=False, timeout=None): """Test to see that an answer returns correct IP's. Args: record_type: text record type for NS query (A, CNAME, etc) record: string to query for expected: tuple of strings expected in all answers critical: If this query fails, should it count against the server. timeout: timeout for query in seconds (int) Returns: (is_broken, error_msg, duration) """ is_broken = False unmatched_answers = [] if not timeout: timeout = self.health_timeout (response, duration, error_msg) = self.TimedRequest(record_type, record, timeout) if response: response_code = rcode.to_text(response.rcode()) if response_code in FATAL_RCODES: error_msg = 'Responded with: %s' % response_code if critical: is_broken = True elif not response.answer: # Avoid preferring broken DNS servers that respond quickly duration = util.SecondsToMilliseconds(self.health_timeout) error_msg = 'No answer (%s): %s' % (response_code, record) is_broken = True else: found_usable_record = False for answer in response.answer: if found_usable_record: break # Process the first sane rdata object available in the answers for rdata in answer: # CNAME if rdata.rdtype == 5: reply = str(rdata.target) # A Record elif rdata.rdtype == 1: reply = str(rdata.address) else: continue found_usable_record = True found_match = False for string in expected: if reply.startswith(string) or reply.endswith(string): found_match = True break if not found_match: unmatched_answers.append(reply) if unmatched_answers: hijack_text = ', '.join(unmatched_answers).rstrip('.') if record in LIKELY_HIJACKS: error_msg = '%s is hijacked: %s' % (record.rstrip('.'), hijack_text) else: error_msg = '%s appears incorrect: %s' % (record.rstrip('.'), hijack_text) else: if not error_msg: error_msg = 'No response' is_broken = True return (is_broken, error_msg, duration)
def probe(domain): """ Recursive query similar to dig+trace from a root server. Arguments: domain (str): full domain name Returns: { 'domain': domain, 'root_ns': ns, For each domain part (str: domain part name): { 'SOA': {} or { 'mname': (str: record mname), 'rname': (str: record rname), 'serial': (str: record serial, 'refresh': (int: record refresh), 'retry': (int: record retry), 'expire': (int: record expire), 'default_ttl': (int: record minimum) }, 'A': {} or { For each name server in dns response (str: name server name): (str: name server ip) }, 'NS': {} or { For each name server in dns response (str: name server name): (str: name server ip) }, 'timeout': (bool), 'ns_queried': '(name server ip after loop)', 'TXT': ['records', 'in', 'txt'] } } Raises: ValueError: (domain, 'not a valid domain name') """ results = {'domain': domain} if domain_re(domain): parts = parse(domain) ns = choice(root_servers) results['root_ns'] = ns for part in parts[1:]: results[part] = {} results[part] = {'SOA': {}, 'A': {}, 'NS': {}, 'timeout': False} name = dns_name.from_text(part) req = message.make_query(name, rdatatype.NS) req_txt = message.make_query(name, rdatatype.TXT) try: res = query.udp(req, ns, timeout=5) res_txt = query.udp(req_txt, ns, timeout=5) except dns.exception.Timeout as e: # if timeout, skip the response results[part]['timeout'] = True logger.log(logger.level, e) continue if res: if res.rcode: rcode = res.rcode() if rcode != dns_rcode.NOERROR: if rcode == dns_rcode.NXDOMAIN: e = Exception(f'{part} does not exist') else: e = Exception(dns_rcode.to_text(rcode)) logger.log(logger.level, e) continue else: e = Exception('rcode not in response') logger.log(logger.level, e) continue rrsets = None if res.authority: rrsets = res.authority elif res.additional: rrsets = [res.additional] else: rrsets = res.answer for rrset in rrsets: for rr in rrset: # check for start of authority if rr.rdtype == rdatatype.SOA: for k in ('mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'): results[part]['SOA'][k if k != 'minimum'\ else 'default_ttl'] = getattr(rr, k) # check for glue records if no SOA # assign name server from glue record # on the parent domain to next query elif rr.rdtype == rdatatype.A: if ip_re(rr.items[0].address): ns = rr.items[0].address results[part]['A'][rr.name] = ns else: e = Exception( 'A record ip is incorrectly formatted') logger.log(logger.level, [e, rr.items[0].address]) # check for NS records if no A record elif rr.rdtype == rdatatype.NS: authority = rr.target try: ns = resolver.query(authority)\ .rrset[0].to_text() if ip_re(ns): results[part]['NS']\ [authority.to_text()] = ns results[part]['ns_queried'] = ns else: e = Exception( 'NS record ip is incorrectly formatted' ) logger.log(logger.level, [e, ns]) except (resolver.NoAnswer, resolver.NoNameservers, resolver.NXDOMAIN, resolver.YXDOMAIN) as e: logger.log(logger.level, e) continue results[part]['TXT'] = [] if res_txt.answer: # dns.query.udp returns an answer object for rrset in res_txt.answer: for rr in rrset: results[part]['TXT'].append(rr.to_text().strip('"')) else: try: res_txt = resolver.query(part, 'TXT') except (resolver.NoAnswer, resolver.NoNameservers, resolver.NXDOMAIN, resolver.YXDOMAIN) as e: logger.log(logger.level, e) continue # dns.resolver.query returns a response.answer object for rrset in res_txt.response.answer: for item in rrset: results[part]['TXT']\ .append(item.to_text().strip('"')) # check to see if we have no SOA records after querying all parts if not any([ bool(results[part]['SOA']) for part in results if part.endswith('.') ]): # skip '.' and 'com.' and dig from previous results for part in list(results)[2:]: if results[part]['NS']: #if not SOA yet, choose a name server from previous ns query ns = choice(list(results[part]['NS'].values())) req = message.make_query(part, rdatatype.SOA) res = query.udp(req, ns) results[part]['ns_queried'] = ns # if timeout, continue to next domain part if not res: continue elif res.answer: #soa records are only answers to queries if res.answer[0].rdtype == rdatatype.SOA: # in rrset [0] , in rr record [0] soa = res.answer[0][0] for k in ('mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'): results[part]['SOA'][k if k != 'minimum' \ else 'default_ttl'] = getattr(soa, k) return results else: e = ValueError(domain, 'not a valid domain name') logger.log(logger.level, e) raise e
class JSONMapper(object): """Map Dnstap data to JSON. This particular implementation filters only client responses to A and AAAA queries, including CNAME chains. Chains are "ellipsed" in the middle if the estimated size of the resulting JSON blob is over MAX_BLOB. Since only Client Response type messages are processed you'll get better performance if you configure your DNS server to only send such messages. The expected specification for BIND in named.conf is: dnstap { client response; }; dnstap-output unix "/tmp/dnstap"; If you don't restrict the message type to client responses, a warning message will be printed for every new connection established. Subclassing to change Filtering or Output ----------------------------------------- filter() -- change packet selection Override filter() to change the packets which get processed further. Some changes can be accomplished by changing MESSAGE_TYPE or ACCEPTED_RECORDS instead. MESSAGE_TYPE -- dnstap.Message.TYPE_* Dnstap message type Changes to this should be coordinated with your nameserver configuration (discussed above). ACCEPTED_RECORDS -- query types This is the set of question (question rdata type or qtype) data types which are accepted. The default is A and AAAA. the constants are defined in dns.rdatatype' FIELDS -- change the output data This list is used to populate a map which is then JSONified. Each entry in the list is an instance of FieldMapping, which ties a JSON name to a function which can extract the appropriate data. """ # This should be safely below MTU, with the intent to avoid fragmentation. MAX_BLOB = 1024 MESSAGE_TYPE = dnstap.Message.TYPE_CLIENT_RESPONSE ACCEPTED_RECORDS = {rdatatype.A, rdatatype.AAAA} FIELDS = (FieldMapping('client', lambda self, p: str(p.field('query_address')[1])), FieldMapping( 'qtype', lambda self, p: rdatatype.to_text( p.field('response_message')[1].question[0].rdtype)), FieldMapping( 'status', lambda self, p: rcode.to_text( p.field('response_message')[1].rcode())), FieldMapping('chain', lambda self, p: self.build_resolution_chain(p))) def build_resolution_chain(self, packet): """Build the (CNAME) resolution chain with ellipsization. CNAMEs should only have one RR each, right? CNAME chains should be short, right? Yeah. Right. So, each element in the chain is actually a list, and the total length of all of the elements in the list of lists cannot exceed MAX_BLOB or we start taking chunks out of the middle to make it smaller. """ response = packet.field('response_message')[1] question = response.question[0].name.to_text().lower() # Deal with NXDOMAIN. if response.rcode() == rcode.NXDOMAIN: return [[question]] # Build a mapping of the rrsets. mapping = { rrset.name.to_text().lower(): rrset for rrset in response.answer } # Follow the question (CNAMEs) to an answer. names = [question] seen = set(names) chain = [[question]] while names: name = names.pop(0) if name in mapping: rr_values = [rr.to_text().lower() for rr in mapping[name]] if mapping[name].rdtype == rdatatype.CNAME: for rr in rr_values: if rr in seen: continue names.append(rr) seen.add(rr) chain.append(rr_values) # Ellipsize if it exceeds MAX_BLOB. lengths = [sum((len(name) for name in e)) for e in chain] if sum(lengths) > self.MAX_BLOB: logging.warn( 'Resolution chain for {} exceeds {}, ellipsizing.'.format( question, self.MAX_BLOB)) shortened = None while sum(lengths) > self.MAX_BLOB: if len(lengths) < 3: break shortened = int(len(lengths) / 2) del lengths[shortened] del chain[shortened] if shortened: chain.insert(shortened, ['(...)']) return chain def filter(self, packet): """Return True if the packet should be processed further.""" if packet.field('type')[1] != self.MESSAGE_TYPE: if self.performance_hint: logging.warn( 'PERFORMANCE HINT: Change your Dnstap config to restrict it to client response only.' ) self.performance_hint = False return False if packet.field('response_message' )[1].question[0].rdtype not in self.ACCEPTED_RECORDS: return False return True def map_fields(self, packet): """Maps all of the fields to their values.""" data = {} for field in self.FIELDS: field(data, self, packet) return data
def google_dns(text, reply): """<host> [type] - Queries Google's DNS-over-HTTPS for <host> with record [type] (default AAAA and A. MX, SOA, PTR, TXT etc).""" args = text.split() raw_host = args.pop(0) # Convert to punycode host = raw_host.encode("idna").decode("utf-8").lower() a_and_aaaa = False if args: qtype = args[0] else: try: # autodetect IP for PTR if rtype wasn't given host = reversename.from_address(host).to_text() qtype = "PTR" except: a_and_aaaa = True qtype = "AAAA" data = query_api(host, qtype, reply) # API error if not data: return data.setdefault("Answer", []) # Now check A if a_and_aaaa: ipv4 = query_api(host, "A", reply) if not ipv4: return # Merge into good result if data["Status"] is 0 or ipv4["Status"] is 0: data["Status"] = 0 data["Answer"].extend(ipv4.get("Answer", [])) # Repack and dedupe based on type and data (often CNAME) answers = {} for ans in data["Answer"]: # Suppress hostname del ans["name"] # (rtype, rdata) tuple is answers key. ans is remaining dict data if (ans["type"], ans["data"]) not in answers: answers[ans.pop("type"), ans.pop("data")] = ans data["Answer"] = answers out = [] if data["Status"] is not 0: out.append(rcode.to_text(data["Status"])) else: if data.get("Answer"): def format_answer(key, **kwargs): rtype, rdata = key return "[h1]{}:[/h1] {} [h3]({})[/h3]".format( # Record type rdatatype.to_text(rtype), # Unescape strings in TXT records etc rdata.decode('string_escape') if "\\" in rdata else rdata, # TTL, and any other value ", ".join( [str(kwargs.pop("TTL", ""))] + ["{}: {}".format(k, v) for k, v in kwargs.items()])) # Return first 4 answers, formatted out.extend([ format_answer(ans, **attribs) for ans, attribs in data["Answer"].items() ][:4]) else: out.append("No data") if "Comment" in data: out.append(data["Comment"]) if data["AD"]: out.append("[h2]DNSSEC[/h2]") if raw_host != host: out.append(host) return " [div] ".join(out)