Beispiel #1
0
 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)]))
Beispiel #2
0
 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)]))
Beispiel #3
0
  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)
Beispiel #4
0
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
Beispiel #5
0
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
    }
Beispiel #6
0
    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 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)
Beispiel #8
0
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
Beispiel #9
0
  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)
Beispiel #10
0
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
Beispiel #11
0
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
Beispiel #12
0
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)