1#!/usr/bin/env python 2# 3# This file is a part of DNSViz, a tool suite for DNS/DNSSEC monitoring, 4# analysis, and visualization. 5# Created by Casey Deccio (casey@deccio.net) 6# 7# Copyright 2014-2016 VeriSign, Inc. 8# 9# Copyright 2016-2021 Casey Deccio 10# 11# DNSViz is free software; you can redistribute it and/or modify 12# it under the terms of the GNU General Public License as published by 13# the Free Software Foundation; either version 2 of the License, or 14# (at your option) any later version. 15# 16# DNSViz is distributed in the hope that it will be useful, 17# but WITHOUT ANY WARRANTY; without even the implied warranty of 18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19# GNU General Public License for more details. 20# 21# You should have received a copy of the GNU General Public License along 22# with DNSViz. If not, see <http://www.gnu.org/licenses/>. 23# 24 25from __future__ import unicode_literals 26 27import argparse 28import atexit 29import binascii 30import codecs 31import errno 32import getopt 33import io 34import json 35import logging 36import multiprocessing 37import multiprocessing.managers 38import os 39import random 40import re 41import shutil 42import signal 43import socket 44import struct 45import subprocess 46import sys 47import tempfile 48import threading 49import time 50 51# minimal support for python2.6 52try: 53 from collections import OrderedDict 54except ImportError: 55 from ordereddict import OrderedDict 56 57# python3/python2 dual compatibility 58try: 59 import urllib.parse 60except ImportError: 61 import urlparse 62else: 63 urlparse = urllib.parse 64 65import dns.edns, dns.exception, dns.message, dns.name, dns.rdata, dns.rdataclass, dns.rdatatype, dns.rdtypes.ANY.NS, dns.rdtypes.IN.A, dns.rdtypes.IN.AAAA, dns.resolver, dns.rrset 66 67from dnsviz.analysis import COOKIE_STANDIN, WILDCARD_EXPLICIT_DELEGATION, PrivateAnalyst, PrivateRecursiveAnalyst, OnlineDomainNameAnalysis, NetworkConnectivityException, DNS_RAW_VERSION 68from dnsviz.config import RESOLV_CONF 69import dnsviz.format as fmt 70from dnsviz.ipaddr import IPAddr 71from dnsviz.query import DiagnosticQuery, QuickDNSSECQuery, StandardRecursiveQueryCD 72from dnsviz.resolver import DNSAnswer, Resolver, ResolvConfError, PrivateFullResolver 73from dnsviz import transport 74from dnsviz.util import get_client_address, get_root_hints 75lb2s = fmt.latin1_binary_to_string 76 77logging.basicConfig(level=logging.WARNING, format='%(message)s') 78logger = logging.getLogger() 79 80# this needs to be global because of multiprocessing 81tm = None 82th_factories = None 83resolver = None 84explicit_delegations = None 85odd_ports = None 86 87A_ROOT_IPV4 = IPAddr('198.41.0.4') 88A_ROOT_IPV6 = IPAddr('2001:503:ba3e::2:30') 89 90class MissingExecutablesError(Exception): 91 pass 92 93class ZoneFileServiceError(Exception): 94 pass 95 96class AnalysisInputError(Exception): 97 pass 98 99class CustomQueryMixin(object): 100 edns_options = [] 101 102#XXX this is a hack required for inter-process sharing of dns.name.Name 103# instances using multiprocess 104def _setattr_dummy(self, name, value): 105 return super(dns.name.Name, self).__setattr__(name, value) 106dns.name.Name.__setattr__ = _setattr_dummy 107 108def _raise_eof(signum, frame): 109 # EOFError is raised instead of KeyboardInterrupt 110 # because the multiprocessing worker doesn't handle 111 # KeyboardInterrupt 112 raise EOFError 113 114def _init_tm(): 115 global tm 116 tm = transport.DNSQueryTransportManager() 117 118def _cleanup_tm(): 119 global tm 120 if tm is not None: 121 tm.close() 122 123def _init_stub_resolver(): 124 global resolver 125 126 servers = set() 127 for rdata in explicit_delegations[(WILDCARD_EXPLICIT_DELEGATION, dns.rdatatype.NS)]: 128 for rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA): 129 if (rdata.target, rdtype) in explicit_delegations: 130 servers.update([IPAddr(r.address) for r in explicit_delegations[(rdata.target, rdtype)]]) 131 resolver = Resolver(list(servers), StandardRecursiveQueryCD, transport_manager=tm) 132 133def _init_full_resolver(): 134 global resolver 135 136 quick_query = QuickDNSSECQuery.add_mixin(CustomQueryMixin).add_server_cookie(COOKIE_STANDIN) 137 diagnostic_query = DiagnosticQuery.add_mixin(CustomQueryMixin).add_server_cookie(COOKIE_STANDIN) 138 139 # now that we have the hints, make resolver a full resolver instead of a stub 140 hints = get_root_hints() 141 for key in explicit_delegations: 142 hints[key] = explicit_delegations[key] 143 resolver = PrivateFullResolver(hints, query_cls=(quick_query, diagnostic_query), odd_ports=odd_ports, cookie_standin=COOKIE_STANDIN, transport_manager=tm) 144 145def _init_interrupt_handler(): 146 signal.signal(signal.SIGINT, _raise_eof) 147 148def _init_subprocess(use_full): 149 _init_tm() 150 if use_full: 151 _init_full_resolver() 152 else: 153 _init_stub_resolver() 154 _init_interrupt_handler() 155 multiprocessing.util.Finalize(None, _cleanup_tm, exitpriority=0) 156 157def _analyze(args): 158 (cls, name, rdclass, dlv_domain, try_ipv4, try_ipv6, client_ipv4, client_ipv6, query_class_mixin, ceiling, edns_diagnostics, \ 159 stop_at_explicit, extra_rdtypes, explicit_only, cache, cache_level, cache_lock) = args 160 if ceiling is not None and name.is_subdomain(ceiling): 161 c = ceiling 162 else: 163 c = name 164 try: 165 a = cls(name, rdclass=rdclass, dlv_domain=dlv_domain, try_ipv4=try_ipv4, try_ipv6=try_ipv6, client_ipv4=client_ipv4, client_ipv6=client_ipv6, query_class_mixin=query_class_mixin, ceiling=c, edns_diagnostics=edns_diagnostics, explicit_delegations=explicit_delegations, stop_at_explicit=stop_at_explicit, odd_ports=odd_ports, extra_rdtypes=extra_rdtypes, explicit_only=explicit_only, analysis_cache=cache, cache_level=cache_level, analysis_cache_lock=cache_lock, transport_manager=tm, th_factories=th_factories, resolver=resolver) 166 return a.analyze() 167 # re-raise a KeyboardInterrupt, as this means we've been interrupted 168 except KeyboardInterrupt: 169 raise 170 # report exceptions related to network connectivity 171 except (NetworkConnectivityException, transport.RemoteQueryTransportError) as e: 172 logger.error('Error analyzing %s: %s' % (fmt.humanize_name(name), e)) 173 # don't report EOFError, as that is what is raised if there is a 174 # KeyboardInterrupt in ParallelAnalyst 175 except EOFError: 176 pass 177 except: 178 logger.exception('Error analyzing %s' % fmt.humanize_name(name)) 179 return None 180 181class BulkAnalyst(object): 182 analyst_cls = PrivateAnalyst 183 use_full_resolver = True 184 185 def __init__(self, rdclass, try_ipv4, try_ipv6, client_ipv4, client_ipv6, query_class_mixin, ceiling, edns_diagnostics, stop_at_explicit, cache_level, extra_rdtypes, explicit_only, dlv_domain): 186 self.rdclass = rdclass 187 self.try_ipv4 = try_ipv4 188 self.try_ipv6 = try_ipv6 189 self.client_ipv4 = client_ipv4 190 self.client_ipv6 = client_ipv6 191 self.query_class_mixin = query_class_mixin 192 self.ceiling = ceiling 193 self.edns_diagnostics = edns_diagnostics 194 self.stop_at_explicit = stop_at_explicit 195 self.cache_level = cache_level 196 self.extra_rdtypes = extra_rdtypes 197 self.explicit_only = explicit_only 198 self.dlv_domain = dlv_domain 199 200 self.cache = {} 201 self.cache_lock = threading.Lock() 202 203 def _name_to_args_iter(self, names): 204 for name in names: 205 yield (self.analyst_cls, name, self.rdclass, self.dlv_domain, self.try_ipv4, self.try_ipv6, self.client_ipv4, self.client_ipv6, self.query_class_mixin, self.ceiling, self.edns_diagnostics, self.stop_at_explicit, self.extra_rdtypes, self.explicit_only, self.cache, self.cache_level, self.cache_lock) 206 207 def analyze(self, names, flush_func=None): 208 name_objs = [] 209 for args in self._name_to_args_iter(names): 210 name_obj = _analyze(args) 211 if flush_func is not None: 212 flush_func(name_obj) 213 else: 214 name_objs.append(name_obj) 215 return name_objs 216 217class RecursiveBulkAnalyst(BulkAnalyst): 218 analyst_cls = PrivateRecursiveAnalyst 219 use_full_resolver = False 220 221class MultiProcessAnalystMixin(object): 222 analysis_model = OnlineDomainNameAnalysis 223 224 def _finalize_analysis_proper(self, name_obj): 225 self.analysis_cache[name_obj.name] = name_obj 226 super(MultiProcessAnalystMixin, self)._finalize_analysis_proper(name_obj) 227 228 def _finalize_analysis_all(self, name_obj): 229 self.analysis_cache[name_obj.name] = name_obj 230 super(MultiProcessAnalystMixin, self)._finalize_analysis_all(name_obj) 231 232 def refresh_dependency_references(self, name_obj, trace=None): 233 if trace is None: 234 trace = [] 235 236 if name_obj.name in trace: 237 return 238 239 if name_obj.parent is not None: 240 self.refresh_dependency_references(name_obj.parent, trace+[name_obj.name]) 241 if name_obj.nxdomain_ancestor is not None: 242 self.refresh_dependency_references(name_obj.nxdomain_ancestor, trace+[name_obj.name]) 243 if name_obj.dlv_parent is not None: 244 self.refresh_dependency_references(name_obj.dlv_parent, trace+[name_obj.name]) 245 246 # loop until all deps have been added 247 for cname in name_obj.cname_targets: 248 for target in name_obj.cname_targets[cname]: 249 while name_obj.cname_targets[cname][target] is None: 250 try: 251 name_obj.cname_targets[cname][target] = self.analysis_cache[target] 252 except KeyError: 253 time.sleep(1) 254 self.refresh_dependency_references(name_obj.cname_targets[cname][target], trace+[name_obj.name]) 255 for signer in name_obj.external_signers: 256 while name_obj.external_signers[signer] is None: 257 try: 258 name_obj.external_signers[signer] = self.analysis_cache[signer] 259 except KeyError: 260 time.sleep(1) 261 self.refresh_dependency_references(name_obj.external_signers[signer], trace+[name_obj.name]) 262 if self.follow_ns: 263 for ns in name_obj.ns_dependencies: 264 while name_obj.ns_dependencies[ns] is None: 265 try: 266 name_obj.ns_dependencies[ns] = self.analysis_cache[ns] 267 except KeyError: 268 time.sleep(1) 269 self.refresh_dependency_references(name_obj.ns_dependencies[ns], trace+[name_obj.name]) 270 if self.follow_mx: 271 for target in name_obj.mx_targets: 272 while name_obj.mx_targets[target] is None: 273 try: 274 name_obj.mx_targets[target] = self.analysis_cache[target] 275 except KeyError: 276 time.sleep(1) 277 self.refresh_dependency_references(name_obj.mx_targets[target], trace+[name_obj.name]) 278 279 def analyze(self): 280 name_obj = super(MultiProcessAnalystMixin, self).analyze() 281 if not self.trace: 282 self.refresh_dependency_references(name_obj) 283 return name_obj 284 285class MultiProcessAnalyst(MultiProcessAnalystMixin, PrivateAnalyst): 286 pass 287 288class RecursiveMultiProcessAnalyst(MultiProcessAnalystMixin, PrivateRecursiveAnalyst): 289 pass 290 291class ParallelAnalystMixin(object): 292 analyst_cls = MultiProcessAnalyst 293 use_full_resolver = None 294 295 def __init__(self, rdclass, try_ipv4, try_ipv6, client_ipv4, client_ipv6, query_class_mixin, ceiling, edns_diagnostics, stop_at_explicit, cache_level, extra_rdtypes, explicit_only, dlv_domain, processes): 296 super(ParallelAnalystMixin, self).__init__(rdclass, try_ipv4, try_ipv6, client_ipv4, client_ipv6, query_class_mixin, ceiling, edns_diagnostics, stop_at_explicit, cache_level, extra_rdtypes, explicit_only, dlv_domain) 297 self.manager = multiprocessing.managers.SyncManager() 298 self.manager.start() 299 300 self.processes = processes 301 302 self.cache = self.manager.dict() 303 self.cache_lock = self.manager.Lock() 304 305 def analyze(self, names, flush_func=None): 306 results = [] 307 name_objs = [] 308 pool = multiprocessing.Pool(self.processes, _init_subprocess, (self.use_full_resolver,)) 309 try: 310 for args in self._name_to_args_iter(names): 311 results.append(pool.apply_async(_analyze, (args,))) 312 # loop instead of just joining, so we can check for interrupt at 313 # main process 314 for result in results: 315 name_objs.append(result.get()) 316 except KeyboardInterrupt: 317 pool.terminate() 318 raise 319 320 pool.close() 321 pool.join() 322 return name_objs 323 324class ParallelAnalyst(ParallelAnalystMixin, BulkAnalyst): 325 analyst_cls = MultiProcessAnalyst 326 use_full_resolver = True 327 328class RecursiveParallelAnalyst(ParallelAnalystMixin, RecursiveBulkAnalyst): 329 analyst_cls = RecursiveMultiProcessAnalyst 330 use_full_resolver = False 331 332class ZoneFileToServe: 333 _next_free_port = 50053 334 335 NAMED = 'named' 336 NAMED_CHECKCONF = 'named-checkconf' 337 NAMED_CONF = '%(dir)s/named.conf' 338 NAMED_PID = '%(dir)s/named.pid' 339 NAMED_LOG = '%(dir)s/named.log' 340 NAMED_CONF_TEMPLATE = ''' 341options { 342 directory "%(dir)s"; 343 pid-file "%(named_pid)s"; 344 listen-on port %(port)d { localhost; }; 345 listen-on-v6 port %(port)d { localhost; }; 346 recursion no; 347 notify no; 348}; 349controls {}; 350zone "%(zone_name)s" { 351 type master; 352 file "%(zone_file)s"; 353}; 354logging { 355 channel info_file { file "%(named_log)s"; severity info; }; 356 category default { info_file; }; 357 category unmatched { null; }; 358}; 359''' 360 ZONEFILE_TEMPLATE_PRE = ''' 361$ORIGIN %(zone_name)s 362$TTL 600 363@ IN SOA localhost. root.localhost. 1 1800 900 86400 600 364@ IN NS @ 365''' 366 ZONEFILE_TEMPLATE_A = '@ IN A 127.0.0.1\n' 367 ZONEFILE_TEMPLATE_AAAA = '@ IN AAAA ::1\n' 368 USAGE_RE = re.compile(r'usage:', re.IGNORECASE) 369 370 def __init__(self, domain, filename): 371 self.domain = domain 372 self.filename = filename 373 374 self.port = self._next_free_port 375 self.__class__._next_free_port += 1 376 377 self.working_dir = None 378 self.pid = None 379 380 @classmethod 381 def from_mappings(cls, domain, mappings, use_ipv6_loopback): 382 zonefile = tempfile.NamedTemporaryFile('w', prefix='dnsviz', delete=False) 383 atexit.register(os.remove, zonefile.name) 384 385 args = { 'zone_name': lb2s(domain.to_text()) } 386 if use_ipv6_loopback: 387 zonefile_template = cls.ZONEFILE_TEMPLATE_PRE + cls.ZONEFILE_TEMPLATE_AAAA 388 else: 389 zonefile_template = cls.ZONEFILE_TEMPLATE_PRE + cls.ZONEFILE_TEMPLATE_A 390 zonefile_contents = zonefile_template % args 391 zonefile.write(zonefile_contents) 392 393 for name, rdtype in mappings: 394 if not name.is_subdomain(domain): 395 continue 396 zonefile.write(mappings[(name, rdtype)].to_text() + '\n') 397 zonefile.close() 398 return cls(domain, zonefile.name) 399 400 def _cleanup_process(self): 401 if self.pid is not None: 402 try: 403 os.kill(self.pid, signal.SIGTERM) 404 except OSError: 405 pass 406 else: 407 time.sleep(1.0) 408 try: 409 os.kill(self.pid, signal.SIGKILL) 410 except OSError: 411 pass 412 413 if self.working_dir is not None: 414 shutil.rmtree(self.working_dir) 415 416 def serve(self): 417 self.working_dir = tempfile.mkdtemp(prefix='dnsviz') 418 env = { 'PATH': '%s:/sbin:/usr/sbin:/usr/local/sbin' % (os.environ.get('PATH', '')) } 419 420 args = { 'dir': self.working_dir, 'port': self.port, 421 'zone_name': lb2s(self.domain.to_text()), 422 'zone_file': os.path.abspath(self.filename) } 423 args['named_conf'] = self.NAMED_CONF % args 424 args['named_pid'] = self.NAMED_PID % args 425 args['named_log'] = self.NAMED_LOG % args 426 427 named_conf_contents = self.NAMED_CONF_TEMPLATE % args 428 io.open(args['named_conf'], 'w', encoding='utf-8').write(named_conf_contents) 429 try: 430 p = subprocess.Popen([self.NAMED_CHECKCONF, '-z', args['named_conf']], 431 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) 432 except OSError as e: 433 self._cleanup_process() 434 raise MissingExecutablesError('The options used require %s. Please ensure that it is installed and in PATH (%s).' % (self.NAMED_CHECKCONF, e)) 435 436 (stdout, stderr) = p.communicate() 437 if p.returncode != 0: 438 stdout = stdout.decode('utf-8') 439 self._cleanup_process() 440 raise ZoneFileServiceError('There was an problem with the zone file for "%s":\n%s' % (args['zone_name'], stdout)) 441 442 named_cmd_without_log = [self.NAMED, '-c', args['named_conf']] 443 named_cmd_with_log = named_cmd_without_log + ['-L', args['named_log']] 444 checked_usage = False 445 for named_cmd in (named_cmd_with_log, named_cmd_without_log): 446 try: 447 p = subprocess.Popen(named_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) 448 except OSError as e: 449 self._cleanup_process() 450 raise MissingExecutablesError('The options used require %s. Please ensure that it is installed and in PATH (%s).' % (self.NAMED, e)) 451 452 (stdout, stderr) = p.communicate() 453 if p.returncode == 0: 454 break 455 456 stdout = stdout.decode('utf-8') 457 if not checked_usage and self.USAGE_RE.search(stdout): 458 # Versions of BIND pre 9.11 don't support -L, so fall back to without -L 459 checked_usage = True 460 continue 461 462 try: 463 with io.open(args['named_log'], 'r', encoding='utf-8') as fh: 464 log = fh.read() 465 except IOError as e: 466 log = '' 467 if not log: 468 log = stdout 469 self._cleanup_process() 470 raise ZoneFileServiceError('There was an problem executing %s to serve the "%s" zone:\n%s' % (self.NAMED, args['zone_name'], log)) 471 472 try: 473 with io.open(args['named_pid'], 'r', encoding='utf-8') as fh: 474 self.pid = int(fh.read()) 475 except (IOError, ValueError) as e: 476 self._cleanup_process() 477 raise ZoneFileServiceError('There was an problem detecting the process ID for %s: %s' % (self.NAMED, e)) 478 479 atexit.register(self._cleanup_process) 480 481class NameServerMappingsForDomain(object): 482 PORT_RE = re.compile(r'^(.*):(\d+)$') 483 BRACKETS_RE = re.compile(r'^\[(.*)\]$') 484 485 DEFAULT_PORT = 53 486 DYN_LABEL = '_dnsviz' 487 488 _allow_file = None 489 _allow_name_only = None 490 _allow_addr_only = None 491 _allow_stop_at = None 492 _handle_file_arg = None 493 494 def __init__(self, domain, stop_at, resolver): 495 if not (self._allow_file is not None and \ 496 self._allow_name_only is not None and \ 497 self._allow_addr_only is not None and \ 498 self._allow_stop_at is not None and \ 499 (not self._allow_file or self._handle_file_arg is not None)): 500 raise NotImplemented 501 502 if stop_at and not self._allow_stop_at: 503 raise argparse.ArgumentTypeError('The "+" may not be specified with this option') 504 505 self.domain = domain 506 self._resolver = resolver 507 self._nsi = 1 508 509 self.delegation_mapping = {} 510 self.stop_at = stop_at 511 self.odd_ports = {} 512 self.filename = None 513 514 self.delegation_mapping[(self.domain, dns.rdatatype.NS)] = dns.rrset.RRset(self.domain, dns.rdataclass.IN, dns.rdatatype.NS) 515 516 @classmethod 517 def _strip_port(cls, s): 518 # Determine whether there is a port attached to the end 519 match = cls.PORT_RE.search(s) 520 if match is not None: 521 s = match.group(1) 522 port = int(match.group(2)) 523 else: 524 port = None 525 return s, port 526 527 def handle_list_arg(self, name_addr_arg): 528 name_addr_arg = name_addr_arg.strip() 529 530 # if the value is actually a path, then check it as a zone file 531 if os.path.isfile(name_addr_arg): 532 if not self._allow_file: 533 raise argparse.ArgumentTypeError('A filename may not be specified with this option') 534 self._handle_file_arg(name_addr_arg) 535 else: 536 self._handle_name_addr_list(name_addr_arg) 537 538 def _handle_name_addr_list(self, name_addr_list): 539 for name_addr in name_addr_list.split(','): 540 self._handle_name_addr_mapping(name_addr) 541 542 def _handle_name_no_addr(self, name, port): 543 query_tuples = ((name, dns.rdatatype.A, dns.rdataclass.IN), (name, dns.rdatatype.AAAA, dns.rdataclass.IN)) 544 answer_map = self._resolver.query_multiple_for_answer(*query_tuples) 545 found_answer = False 546 for (n, rdtype, rdclass) in answer_map: 547 a = answer_map[(n, rdtype, rdclass)] 548 if isinstance(a, DNSAnswer): 549 found_answer = True 550 if (name, rdtype) not in self.delegation_mapping: 551 self.delegation_mapping[(name, rdtype)] = dns.rrset.RRset(name, dns.rdataclass.IN, rdtype) 552 if rdtype == dns.rdatatype.A: 553 rdtype_cls = dns.rdtypes.IN.A.A 554 else: 555 rdtype_cls = dns.rdtypes.IN.AAAA.AAAA 556 for rdata in a.rrset: 557 self.delegation_mapping[(name, rdtype)].add(rdtype_cls(dns.rdataclass.IN, rdtype, rdata.address)) 558 559 if port is not None and port != self.DEFAULT_PORT: 560 self.odd_ports[(self.domain, IPAddr(rdata.address))] = port 561 562 # negative responses 563 elif isinstance(a, (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer)): 564 pass 565 # error responses 566 elif isinstance(a, (dns.exception.Timeout, dns.resolver.NoNameservers)): 567 pass 568 569 if not found_answer: 570 raise argparse.ArgumentTypeError('"%s" could not be resolved to an address. Please specify an address or use a name that resolves properly.' % fmt.humanize_name(name)) 571 572 def _handle_name_with_addr(self, name, addr, port): 573 if addr.version == 6: 574 rdtype = dns.rdatatype.AAAA 575 rdtype_cls = dns.rdtypes.IN.AAAA.AAAA 576 else: 577 rdtype = dns.rdatatype.A 578 rdtype_cls = dns.rdtypes.IN.A.A 579 if (name, rdtype) not in self.delegation_mapping: 580 self.delegation_mapping[(name, rdtype)] = dns.rrset.RRset(name, dns.rdataclass.IN, rdtype) 581 self.delegation_mapping[(name, rdtype)].add(rdtype_cls(dns.rdataclass.IN, rdtype, addr)) 582 583 if port is not None and port != self.DEFAULT_PORT: 584 self.odd_ports[(self.domain, addr)] = port 585 586 def _handle_name_addr_mapping(self, name_addr): 587 name_addr = name_addr.strip() 588 name, addr, port = self._parse_name_addr(name_addr) 589 590 if not name and not self._allow_addr_only: 591 raise argparse.ArgumentTypeError('A domain name must accompany the address') 592 if not addr and not self._allow_name_only: 593 raise argparse.ArgumentTypeError('An address must accompany the domain name name') 594 595 name = self._format_name(name) 596 addr = self._format_addr(addr) 597 598 # Add the name to the NS RRset 599 self.delegation_mapping[(self.domain, dns.rdatatype.NS)].add(dns.rdtypes.ANY.NS.NS(dns.rdataclass.IN, dns.rdatatype.NS, name)) 600 601 if not addr: 602 self._handle_name_no_addr(name, port) 603 else: 604 self._handle_name_with_addr(name, addr, port) 605 606 def _create_name(self): 607 # value is an address 608 name = 'ns%d.%s.%s' % (self._nsi, self.DYN_LABEL, lb2s(self.domain.canonicalize().to_text())) 609 self._nsi += 1 610 return name 611 612 def _format_name(self, name): 613 if name is None: 614 name = self._create_name() 615 try: 616 name = dns.name.from_text(name) 617 except dns.exception.DNSException: 618 raise argparse.ArgumentTypeError('The domain name was invalid: "%s"' % name) 619 return name 620 621 def _format_addr(self, addr): 622 if addr is not None: 623 addr, num_sub = self.BRACKETS_RE.subn(r'\1', addr) 624 try: 625 addr = IPAddr(addr) 626 except ValueError: 627 raise argparse.ArgumentTypeError('The IP address was invalid: "%s"' % addr) 628 629 if addr.version == 6 and num_sub < 1: 630 raise argparse.ArgumentTypeError('Brackets are required around IPv6 addresses.') 631 return addr 632 633 def _parse_name_addr(self, name_addr): 634 # 1. Strip an optional port off the end 635 name_addr_orig = name_addr 636 name_addr, port = self._strip_port(name_addr) 637 638 # 2. Now determine whether the argument is a) a single value--either 639 # name or addr--or b) a name-addr mapping 640 try: 641 name, addr = name_addr.split('=', 1) 642 except ValueError: 643 # a) Argument is either a name or an address, not a mapping; 644 # Now, determine which it is. 645 try: 646 IPAddr(self.BRACKETS_RE.sub(r'\1', name_addr)) 647 except ValueError: 648 # a1. It is not a valid address. Maybe. See if the address 649 # was valid with the port re-appended. 650 try: 651 IPAddr(self.BRACKETS_RE.sub(r'\1', name_addr_orig)) 652 except ValueError: 653 # a2. Even with the port, the address is not valid, so the 654 # must be a name instead of an address. Validity of 655 # the name will be checked later. 656 name = name_addr 657 addr = None 658 else: 659 # a3. When considering the address with the port, the 660 # address is valid, so it is in fact an address. 661 # Re-append the port to make the address valid, and 662 # cancel the port. 663 name = None 664 addr = name_addr_orig 665 port = None 666 else: 667 # a4. Value was a valid address. 668 name = None 669 addr = name_addr 670 671 else: 672 # b) Argument is a name-addr mapping. Now, determine whether 673 # removing the port was the right thing. 674 name = name.strip() 675 addr = addr.strip() 676 677 if port is None: 678 addr_orig = addr 679 else: 680 addr_orig = '%s:%d' % (addr, port) 681 682 try: 683 IPAddr(self.BRACKETS_RE.sub(r'\1', addr)) 684 except ValueError: 685 # b1. Without the port, addr is not a valid address. See if 686 # things change when we re-append the port. 687 try: 688 IPAddr(self.BRACKETS_RE.sub(r'\1', addr_orig)) 689 except ValueError: 690 # b2. Even with the port, the address is not valid, so it 691 # doesn't matter if we leave the port on or off; 692 # address invalidity will be reported later. 693 pass 694 else: 695 # b3. When considering the address with the port, the 696 # address is valid, so re-append the port to make the 697 # address valid, and cancel the port. 698 addr = addr_orig 699 port = None 700 else: 701 # b4. Value was a valid address, so no need to do anything 702 pass 703 704 return name, addr, port 705 706 def _set_filename(self, filename): 707 self.filename = filename 708 709 def _extract_delegation_info_from_file(self, filename): 710 # if this is a file containing delegation records, then read the 711 # file, create a name=value string, and call name_addrs_from_string() 712 try: 713 with io.open(filename, 'r', encoding='utf-8') as fh: 714 file_contents = fh.read() 715 except IOError as e: 716 raise argparse.ArgumentTypeError('%s: "%s"' % (e.strerror, filename)) 717 718 try: 719 m = dns.message.from_text(str(';ANSWER\n' + file_contents)) 720 except dns.exception.DNSException as e: 721 raise argparse.ArgumentTypeError('Error reading delegation records from %s: "%s"' % (filename, e)) 722 723 try: 724 ns_rrset = m.find_rrset(m.answer, self.domain, dns.rdataclass.IN, dns.rdatatype.NS) 725 except KeyError: 726 raise argparse.ArgumentTypeError('No NS records for %s found in %s' % (lb2s(self.domain.canonicalize().to_text()), filename)) 727 728 for rdata in ns_rrset: 729 a_rrsets = [r for r in m.answer if r.name == rdata.target and r.rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA)] 730 if not a_rrsets or not rdata.target.is_subdomain(self.domain.parent()): 731 name_addr = lb2s(rdata.target.canonicalize().to_text()) 732 else: 733 for a_rrset in a_rrsets: 734 for a_rdata in a_rrset: 735 name_addr = '%s=[%s]' % (lb2s(rdata.target.canonicalize().to_text()), a_rdata.address) 736 self._handle_name_addr_mapping(name_addr) 737 738class DelegationNameServerMappingsForDomain(NameServerMappingsForDomain): 739 _allow_file = True 740 _allow_name_only = False 741 _allow_addr_only = False 742 _allow_stop_at = False 743 _handle_file_arg = NameServerMappingsForDomain._extract_delegation_info_from_file 744 745 def __init__(self, *args, **kwargs): 746 super(DelegationNameServerMappingsForDomain, self).__init__(*args, **kwargs) 747 if self.domain == dns.name.root: 748 raise argparse.ArgumentTypeError('The root domain may not specified with this option.') 749 750class AuthoritativeNameServerMappingsForDomain(NameServerMappingsForDomain): 751 _allow_file = True 752 _allow_name_only = True 753 _allow_addr_only = True 754 _allow_stop_at = True 755 _handle_file_arg = NameServerMappingsForDomain._set_filename 756 757class RecursiveServersForDomain(NameServerMappingsForDomain): 758 _allow_file = False 759 _allow_name_only = True 760 _allow_addr_only = True 761 _allow_stop_at = False 762 _handle_file_arg = None 763 764class DSForDomain: 765 def __init__(self, domain, stop_at, resolver): 766 self.domain = domain 767 768 if stop_at and not self._allow_stop_at: 769 raise argparse.ArgumentTypeError('The "+" may not be specified with this option') 770 771 self.delegation_mapping = {} 772 self.delegation_mapping[(self.domain, dns.rdatatype.DS)] = dns.rrset.RRset(self.domain, dns.rdataclass.IN, dns.rdatatype.DS) 773 774 def _extract_ds_info_from_file(self, filename): 775 # if this is a file containing delegation records, then read the 776 # file, create a name=value string, and call name_addrs_from_string() 777 try: 778 with io.open(filename, 'r', encoding='utf-8') as fh: 779 file_contents = fh.read() 780 except IOError as e: 781 raise argparse.ArgumentTypeError('%s: "%s"' % (e.strerror, filename)) 782 783 try: 784 m = dns.message.from_text(str(';ANSWER\n' + file_contents)) 785 except dns.exception.DNSException as e: 786 raise argparse.ArgumentTypeError('Error reading DS records from %s: "%s"' % (filename, e)) 787 788 try: 789 ds_rrset = m.find_rrset(m.answer, self.domain, dns.rdataclass.IN, dns.rdatatype.DS) 790 except KeyError: 791 raise argparse.ArgumentTypeError('No DS records for %s found in %s' % (lb2s(self.domain.canonicalize().to_text()), filename)) 792 793 for rdata in ds_rrset: 794 self.delegation_mapping[(self.domain, dns.rdatatype.DS)].add(rdata) 795 796 def _handle_ds(self, ds): 797 ds = ds.strip() 798 799 try: 800 self.delegation_mapping[(self.domain, dns.rdatatype.DS)].add(dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, ds)) 801 except dns.exception.DNSException as e: 802 raise argparse.ArgumentTypeError('Error parsing DS records: %s\n%s' % (e, ds)) 803 804 def _handle_ds_list(self, ds_list): 805 for ds in ds_list.split(','): 806 self._handle_ds(ds) 807 808 def handle_list_arg(self, ds_arg): 809 ds_arg = ds_arg.strip() 810 811 # if the value is actually a path, then check it as a zone file 812 if os.path.isfile(ds_arg): 813 self._extract_ds_info_from_file(ds_arg) 814 else: 815 self._handle_ds_list(ds_arg) 816 817class DomainListArgHelper: 818 STOP_RE = re.compile(r'^(.*)\+$') 819 820 def __init__(self, resolver): 821 self._resolver = resolver 822 823 @classmethod 824 def _strip_stop_marker(cls, s): 825 match = cls.STOP_RE.search(s) 826 if match is not None: 827 s = match.group(1) 828 stop_at = True 829 else: 830 stop_at = False 831 832 return s, stop_at 833 834 def _parse_domain_list(self, domain_item_list): 835 try: 836 domain, item_list = domain_item_list.split(':', 1) 837 except ValueError: 838 raise argparse.ArgumentTypeError('Option expects both a domain and servers for that domain') 839 840 domain = domain.strip() 841 domain, stop_at = self._strip_stop_marker(domain) 842 843 return domain, item_list, stop_at 844 845 def _handle_domain_list_arg(self, cls, domain_list_arg): 846 domain, list_arg, stop_at = self._parse_domain_list(domain_list_arg) 847 848 if domain is not None: 849 domain = domain.strip() 850 try: 851 domain = dns.name.from_text(domain) 852 except dns.exception.DNSException: 853 raise argparse.ArgumentTypeError('The domain name was invalid: "%s"' % domain) 854 855 if list_arg is not None: 856 list_arg = list_arg.strip() 857 858 obj = cls(domain, stop_at, self._resolver) 859 860 if list_arg: 861 obj.handle_list_arg(list_arg) 862 return obj 863 864 def _handle_list_arg(self, cls, list_arg): 865 obj = cls(WILDCARD_EXPLICIT_DELEGATION, False, self._resolver) 866 obj.handle_list_arg(list_arg) 867 return obj 868 869 def delegation_name_server_mappings(self, arg): 870 return self._handle_domain_list_arg(DelegationNameServerMappingsForDomain, arg) 871 872 def authoritative_name_server_mappings(self, arg): 873 return self._handle_domain_list_arg(AuthoritativeNameServerMappingsForDomain, arg) 874 875 def recursive_servers_for_domain(self, arg): 876 return self._handle_list_arg(RecursiveServersForDomain, arg) 877 878 def ds_for_domain(self, arg): 879 return self._handle_domain_list_arg(DSForDomain, arg) 880 881class ArgHelper: 882 BRACKETS_RE = re.compile(r'^\[(.*)\]$') 883 884 def __init__(self, resolver, logger): 885 self._resolver = resolver 886 self.parser = None 887 888 self.odd_ports = {} 889 self.stop_at = {} 890 self.explicit_delegations = {} 891 self.ceiling = None 892 self.explicit_only = None 893 self.try_ipv4 = None 894 self.try_ipv6 = None 895 self.client_ipv4 = None 896 self.client_ipv6 = None 897 self.edns_diagnostics = None 898 self.th_factories = None 899 self.processes = None 900 self.dlv_domain = None 901 self.meta_only = None 902 self.cache_level = None 903 self.names = None 904 self.analysis_structured = None 905 906 self.args = None 907 self._arg_mapping = None 908 909 self._resolver = resolver 910 self._logger = logger 911 self._zones_to_serve = [] 912 913 def build_parser(self, prog): 914 self.parser = argparse.ArgumentParser(description='Issue diagnostic DNS queries', prog=prog) 915 helper = DomainListArgHelper(self._resolver) 916 917 # python3/python2 dual compatibility 918 stdout_buffer = io.open(sys.stdout.fileno(), 'wb', closefd=False) 919 920 try: 921 self.parser.add_argument('-f', '--names-file', 922 type=argparse.FileType('r', encoding='utf-8'), 923 action='store', metavar='<filename>', 924 help='Read names from a file') 925 except TypeError: 926 # this try/except is for 927 # python3/python2 dual compatibility 928 self.parser.add_argument('-f', '--names-file', 929 type=argparse.FileType('r'), 930 action='store', metavar='<filename>', 931 help='Read names from a file') 932 self.parser.add_argument('-d', '--debug', 933 type=int, choices=range(4), default=2, 934 action='store', metavar='<level>', 935 help='Set debug level') 936 try: 937 self.parser.add_argument('-r', '--input-file', 938 type=argparse.FileType('r', encoding='utf-8'), 939 action='store', metavar='<filename>', 940 help='Read diagnostic queries from a file') 941 except TypeError: 942 # this try/except is for 943 # python3/python2 dual compatibility 944 self.parser.add_argument('-r', '--input-file', 945 type=argparse.FileType('r'), 946 action='store', metavar='<filename>', 947 help='Read diagnostic queries from a file') 948 self.parser.add_argument('-t', '--threads', 949 type=self.positive_int, default=1, 950 action='store', metavar='<threads>', 951 help='Use the specified number of threads for parallel queries') 952 self.parser.add_argument('-4', '--ipv4', 953 const=True, default=False, 954 action='store_const', 955 help='Use IPv4 only') 956 self.parser.add_argument('-6', '--ipv6', 957 const=True, default=False, 958 action='store_const', 959 help='Use IPv6 only') 960 self.parser.add_argument('-b', '--source-ip', 961 type=self.bindable_ip, default=[], 962 action='append', metavar='<address>', 963 help='Use the specified source IPv4 or IPv6 address for queries') 964 self.parser.add_argument('-u', '--looking-glass-url', 965 type=self.valid_url, 966 action='append', metavar='<url>', 967 help='Issue queries through the DNS looking glass at the specified URL') 968 self.parser.add_argument('-k', '--insecure', 969 const=True, default=False, 970 action='store_const', 971 help='Do not verify the TLS certificate for a DNS looking glass using HTTPS') 972 self.parser.add_argument('-a', '--ancestor', 973 type=self.valid_domain_name, default=None, 974 action='store', metavar='<ancestor>', 975 help='Query the ancestry of each domain name through the specified ancestor') 976 self.parser.add_argument('-R', '--rr-types', 977 type=self.comma_separated_dns_types, 978 action='store', metavar='<type>,[<type>...]', 979 help='Issue queries for only the specified type(s) during analysis') 980 self.parser.add_argument('-s', '--recursive-servers', 981 type=helper.recursive_servers_for_domain, default=[], 982 action='append', metavar='<server>[,<server>...]', 983 help='Query the specified recursive server(s)') 984 self.parser.add_argument('-A', '--authoritative-analysis', 985 const=True, default=False, 986 action='store_const', 987 help='Query authoritative servers, instead of recursive servers') 988 self.parser.add_argument('-x', '--authoritative-servers', 989 type=helper.authoritative_name_server_mappings, default=[], 990 action='append', metavar='<domain>[+]:<server>[,<server>...]', 991 help='Query the specified authoritative servers for a domain') 992 self.parser.add_argument('-N', '--delegation-information', 993 type=helper.delegation_name_server_mappings, default=[], 994 action='append', metavar='<domain>:<server>[,<server>...]', 995 help='Use the specified delegation information for a domain') 996 self.parser.add_argument('-D', '--ds', 997 type=helper.ds_for_domain, default=[], 998 action='append', metavar='<domain>:"<ds>"[,"<ds>"...]', 999 help='Use the specified DS records for a domain') 1000 self.parser.add_argument('-n', '--nsid', 1001 const=self.nsid_option(), 1002 action='store_const', 1003 help='Use the NSID EDNS option in queries') 1004 self.parser.add_argument('-e', '--client-subnet', 1005 type=self.ecs_option, 1006 action='store', metavar='<subnet>[:<prefix_len>]', 1007 help='Use the DNS client subnet option with the specified subnet and prefix length in queries') 1008 self.parser.add_argument('-c', '--cookie', 1009 type=self.dns_cookie_option, default=self.dns_cookie_rand(), 1010 action='store', metavar='<cookie>', 1011 help='Use the specified DNS cookie value in queries') 1012 self.parser.add_argument('-E', '--edns', 1013 const=True, default=False, 1014 action='store_const', 1015 help='Issue queries to check EDNS compatibility') 1016 self.parser.add_argument('-o', '--output-file', 1017 type=argparse.FileType('wb'), default=stdout_buffer, 1018 action='store', metavar='<filename>', 1019 help='Save the output to the specified file') 1020 self.parser.add_argument('-p', '--pretty-output', 1021 const=True, default=False, 1022 action='store_const', 1023 help='Format JSON output with indentation and newlines') 1024 self.parser.add_argument('domain_name', 1025 type=self.valid_domain_name, 1026 action='store', nargs='*', metavar='<domain_name>', 1027 help='Domain names') 1028 1029 self._arg_mapping = dict([(a.dest, '/'.join(a.option_strings)) for a in self.parser._actions]) 1030 1031 def parse_args(self, args): 1032 self.args = self.parser.parse_args(args) 1033 1034 @classmethod 1035 def positive_int(cls, arg): 1036 try: 1037 val = int(arg) 1038 except ValueError: 1039 msg = "The argument must be a positive integer: %s" % val 1040 raise argparse.ArgumentTypeError(msg) 1041 else: 1042 if val < 1: 1043 msg = "The argument must be a positive integer: %d" % val 1044 raise argparse.ArgumentTypeError(msg) 1045 return val 1046 1047 @classmethod 1048 def bindable_ip(cls, arg): 1049 try: 1050 addr = IPAddr(cls.BRACKETS_RE.sub(r'\1', arg)) 1051 except ValueError: 1052 raise argparse.ArgumentTypeError('The IP address was invalid: "%s"' % arg) 1053 if addr.version == 4: 1054 fam = socket.AF_INET 1055 else: 1056 fam = socket.AF_INET6 1057 try: 1058 s = socket.socket(fam) 1059 s.bind((addr, 0)) 1060 except socket.error as e: 1061 if e.errno == errno.EADDRNOTAVAIL: 1062 raise argparse.ArgumentTypeError('Cannot bind to specified IP address: "%s"' % addr) 1063 finally: 1064 s.close() 1065 return addr 1066 1067 @classmethod 1068 def valid_url(cls, arg): 1069 url = urlparse.urlparse(arg) 1070 if url.scheme not in ('http', 'https', 'ws', 'ssh'): 1071 raise argparse.ArgumentTypeError('Unsupported URL scheme: "%s"' % url.scheme) 1072 1073 # check that version is >= 2.7.9 if HTTPS is requested 1074 if url.scheme == 'https': 1075 vers0, vers1, vers2 = sys.version_info[:3] 1076 if (2, 7, 9) > (vers0, vers1, vers2): 1077 raise argparse.ArgumentTypeError('Python version >= 2.7.9 is required to use a DNS looking glass with HTTPS.') 1078 1079 elif url.scheme == 'ws': 1080 if url.hostname is not None: 1081 raise argparse.ArgumentTypeError('WebSocket URL must designate a local UNIX domain socket.') 1082 1083 return arg 1084 1085 @classmethod 1086 def comma_separated_dns_types(cls, arg): 1087 rdtypes = [] 1088 arg = arg.strip() 1089 if not arg: 1090 return rdtypes 1091 for r in arg.split(','): 1092 try: 1093 rdtypes.append(dns.rdatatype.from_text(r.strip())) 1094 except dns.rdatatype.UnknownRdatatype: 1095 raise argparse.ArgumentTypeError('Invalid resource record type: %s' % (r)) 1096 return rdtypes 1097 1098 @classmethod 1099 def valid_domain_name(cls, arg): 1100 # python3/python2 dual compatibility 1101 if isinstance(arg, bytes): 1102 arg = codecs.decode(arg, sys.getfilesystemencoding()) 1103 try: 1104 return dns.name.from_text(arg) 1105 except dns.exception.DNSException: 1106 raise argparse.ArgumentTypeError('Invalid domain name: "%s"' % arg) 1107 1108 @classmethod 1109 def nsid_option(cls): 1110 return dns.edns.GenericOption(dns.edns.NSID, b'') 1111 1112 @classmethod 1113 def ecs_option(cls, arg): 1114 try: 1115 addr, prefix_len = arg.split('/', 1) 1116 except ValueError: 1117 addr = arg 1118 prefix_len = None 1119 1120 try: 1121 addr = IPAddr(addr) 1122 except ValueError: 1123 raise argparse.ArgumentTypeError('The IP address was invalid: "%s"' % addr) 1124 1125 if addr.version == 4: 1126 addrlen = 4 1127 family = 1 1128 else: 1129 addrlen = 16 1130 family = 2 1131 1132 if prefix_len is None: 1133 prefix_len = addrlen << 3 1134 else: 1135 try: 1136 prefix_len = int(prefix_len) 1137 except ValueError: 1138 raise argparse.ArgumentTypeError('The prefix length was invalid: "%s"' % prefix_len) 1139 1140 if prefix_len < 0 or prefix_len > (addrlen << 3): 1141 raise argparse.ArgumentTypeError('The prefix length was invalid: "%d"' % prefix_len) 1142 1143 bytes_masked, remainder = divmod(prefix_len, 8) 1144 1145 wire = struct.pack(b'!H', family) 1146 wire += struct.pack(b'!B', prefix_len) 1147 wire += struct.pack(b'!B', 0) 1148 wire += addr._ipaddr_bytes[:bytes_masked] 1149 if remainder: 1150 # python3/python2 dual compatibility 1151 byte = addr._ipaddr_bytes[bytes_masked] 1152 if isinstance(addr._ipaddr_bytes, str): 1153 byte = ord(byte) 1154 1155 mask = ~(2**(8 - remainder)-1) 1156 wire += struct.pack('B', mask & byte) 1157 1158 return dns.edns.GenericOption(8, wire) 1159 1160 @classmethod 1161 def dns_cookie_option(cls, arg): 1162 if not arg: 1163 return None 1164 1165 try: 1166 cookie = binascii.unhexlify(arg) 1167 except (binascii.Error, TypeError): 1168 raise argparse.ArgumentTypeError('The DNS cookie provided was not valid hexadecimal: "%s"' % arg) 1169 1170 if len(cookie) != 8: 1171 raise argparse.ArgumentTypeError('The DNS client cookie provided had a length of %d, but only a length of %d is valid .' % (len(cookie), 8)) 1172 1173 return dns.edns.GenericOption(10, cookie) 1174 1175 @classmethod 1176 def dns_cookie_rand(cls): 1177 r = random.getrandbits(64) 1178 cookie = struct.pack(b'Q', r) 1179 return cls.dns_cookie_option(binascii.hexlify(cookie)) 1180 1181 def aggregate_delegation_info(self): 1182 localhost = dns.name.from_text('localhost') 1183 try: 1184 self.bindable_ip('::1') 1185 except argparse.ArgumentTypeError: 1186 use_ipv6_loopback = False 1187 loopback = IPAddr('127.0.0.1') 1188 loopback_rdtype = dns.rdatatype.A 1189 loopback_rdtype_cls = dns.rdtypes.IN.A.A 1190 else: 1191 use_ipv6_loopback = True 1192 loopback = IPAddr('::1') 1193 loopback_rdtype = dns.rdatatype.AAAA 1194 loopback_rdtype_cls = dns.rdtypes.IN.AAAA.AAAA 1195 1196 self.rdclass = dns.rdataclass.IN 1197 1198 for arg in self.args.recursive_servers + self.args.authoritative_servers: 1199 zone_name = arg.domain 1200 for name, rdtype in arg.delegation_mapping: 1201 if (name, rdtype) not in self.explicit_delegations: 1202 self.explicit_delegations[(name, rdtype)] = arg.delegation_mapping[(name, rdtype)] 1203 else: 1204 self.explicit_delegations[(name, rdtype)].update(arg.delegation_mapping[(name, rdtype)]) 1205 self.odd_ports.update(arg.odd_ports) 1206 self.stop_at[arg.domain] = arg.stop_at 1207 if arg.filename is not None: 1208 zone = ZoneFileToServe(arg.domain, arg.filename) 1209 self._zones_to_serve.append(zone) 1210 self.explicit_delegations[(zone_name, dns.rdatatype.NS)].add(dns.rdtypes.ANY.NS.NS(dns.rdataclass.IN, dns.rdatatype.NS, localhost)) 1211 self.explicit_delegations[(localhost, loopback_rdtype)] = dns.rrset.RRset(localhost, dns.rdataclass.IN, loopback_rdtype) 1212 self.explicit_delegations[(localhost, loopback_rdtype)].add(loopback_rdtype_cls(dns.rdataclass.IN, loopback_rdtype, loopback)) 1213 self.odd_ports[(zone_name, loopback)] = zone.port 1214 1215 delegation_info_by_zone = OrderedDict() 1216 for arg in self.args.ds + self.args.delegation_information: 1217 zone_name = arg.domain.parent() 1218 if (zone_name, dns.rdatatype.NS) in self.explicit_delegations: 1219 raise argparse.ArgumentTypeError('Cannot use "' + lb2s(zone_name.to_text()) + '" with %(authoritative_servers)s if a child zone is specified with %(delegation_information)s' % self._arg_mapping) 1220 if zone_name not in delegation_info_by_zone: 1221 delegation_info_by_zone[zone_name] = {} 1222 for name, rdtype in arg.delegation_mapping: 1223 if (name, rdtype) not in delegation_info_by_zone[zone_name]: 1224 delegation_info_by_zone[zone_name][(name, rdtype)] = arg.delegation_mapping[(name, rdtype)] 1225 else: 1226 delegation_info_by_zone[zone_name][(name, rdtype)].update(arg.delegation_mapping[(name, rdtype)]) 1227 1228 for zone_name in delegation_info_by_zone: 1229 zone = ZoneFileToServe.from_mappings(zone_name, delegation_info_by_zone[zone_name], use_ipv6_loopback) 1230 self._zones_to_serve.append(zone) 1231 self.explicit_delegations[(zone_name, dns.rdatatype.NS)] = dns.rrset.RRset(zone_name, dns.rdataclass.IN, dns.rdatatype.NS) 1232 self.explicit_delegations[(zone_name, dns.rdatatype.NS)].add(dns.rdtypes.ANY.NS.NS(dns.rdataclass.IN, dns.rdatatype.NS, localhost)) 1233 self.explicit_delegations[(localhost, loopback_rdtype)] = dns.rrset.RRset(localhost, dns.rdataclass.IN, loopback_rdtype) 1234 self.explicit_delegations[(localhost, loopback_rdtype)].add(loopback_rdtype_cls(dns.rdataclass.IN, loopback_rdtype, loopback)) 1235 self.odd_ports[(zone_name, loopback)] = zone.port 1236 self.stop_at[zone_name] = True 1237 1238 def populate_recursive_servers(self): 1239 if not self.args.authoritative_analysis and not self.args.recursive_servers: 1240 if (WILDCARD_EXPLICIT_DELEGATION, dns.rdatatype.NS) not in self.explicit_delegations: 1241 self.explicit_delegations[(WILDCARD_EXPLICIT_DELEGATION, dns.rdatatype.NS)] = dns.rrset.RRset(WILDCARD_EXPLICIT_DELEGATION, dns.rdataclass.IN, dns.rdatatype.NS) 1242 for i, server in enumerate(self._resolver._servers): 1243 if IPAddr(server).version == 6: 1244 rdtype = dns.rdatatype.AAAA 1245 else: 1246 rdtype = dns.rdatatype.A 1247 name = dns.name.from_text('ns%d' % i) 1248 self.explicit_delegations[(WILDCARD_EXPLICIT_DELEGATION, dns.rdatatype.NS)].add(dns.rdtypes.ANY.NS.NS(dns.rdataclass.IN, dns.rdatatype.NS, name)) 1249 if (name, rdtype) not in self.explicit_delegations: 1250 self.explicit_delegations[(name, rdtype)] = dns.rrset.RRset(name, dns.rdataclass.IN, rdtype) 1251 self.explicit_delegations[(name, rdtype)].add(dns.rdata.from_text(dns.rdataclass.IN, rdtype, server)) 1252 1253 def check_args(self): 1254 if not self.args.names_file and not self.args.domain_name and not self.args.input_file: 1255 raise argparse.ArgumentTypeError('If no domain names are supplied as command-line arguments, then either %(input_file)s or %(names_file)s must be used.' % \ 1256 self._arg_mapping) 1257 if self.args.names_file and self.args.domain_name: 1258 raise argparse.ArgumentTypeError('If %(names_file)s is used, then domain names may not supplied as command line arguments.' % \ 1259 self._arg_mapping) 1260 if self.args.authoritative_analysis and self.args.recursive_servers: 1261 raise argparse.ArgumentTypeError('If %(authoritative_analysis)s is used, then %(recursive_servers)s cannot be used.' % \ 1262 self._arg_mapping) 1263 if self.args.authoritative_servers and not self.args.authoritative_analysis: 1264 raise argparse.ArgumentTypeError('%(authoritative_servers)s may only be used in conjunction with %(authoritative_analysis)s.' % \ 1265 self._arg_mapping) 1266 if self.args.delegation_information and not self.args.authoritative_analysis: 1267 raise argparse.ArgumentTypeError('%(delegation_information)s may only be used in conjunction with %(authoritative_analysis)s.' % \ 1268 self._arg_mapping) 1269 if self.args.ds and not self.args.delegation_information: 1270 raise argparse.ArgumentTypeError('%(ds)s may only be used in conjunction with %(delegation_information)s.' % \ 1271 self._arg_mapping) 1272 1273 def set_kwargs(self): 1274 if self.args.ancestor is not None: 1275 self.ceiling = self.args.ancestor 1276 elif self.args.authoritative_analysis: 1277 self.ceiling = None 1278 else: 1279 self.ceiling = dns.name.root 1280 1281 if self.args.rr_types is not None: 1282 self.explicit_only = True 1283 else: 1284 self.explicit_only = False 1285 1286 # if both are specified or neither is specified, then they're both tried 1287 if (self.args.ipv4 and self.args.ipv6) or \ 1288 (not self.args.ipv4 and not self.args.ipv6): 1289 self.try_ipv4 = True 1290 self.try_ipv6 = True 1291 # if one or the other is specified, then only the one specified is 1292 # tried 1293 else: 1294 if self.args.ipv4: 1295 self.try_ipv4 = True 1296 self.try_ipv6 = False 1297 else: # self.args.ipv6 1298 self.try_ipv4 = False 1299 self.try_ipv6 = True 1300 1301 for ip in self.args.source_ip: 1302 if ip.version == 4: 1303 self.client_ipv4 = ip 1304 else: 1305 self.client_ipv6 = ip 1306 1307 if self.args.looking_glass_url: 1308 self.th_factories = [] 1309 for looking_glass_url in self.args.looking_glass_url: 1310 url = urlparse.urlparse(looking_glass_url) 1311 if url.scheme in ('http', 'https'): 1312 self.th_factories.append(transport.DNSQueryTransportHandlerHTTPFactory(looking_glass_url, insecure=self.args.insecure)) 1313 elif url.scheme == 'ws': 1314 self.th_factories.append(transport.DNSQueryTransportHandlerWebSocketServerFactory(url.path)) 1315 elif url.scheme == 'ssh': 1316 self.th_factories.append(transport.DNSQueryTransportHandlerRemoteCmdFactory(looking_glass_url)) 1317 else: 1318 self.th_factories = None 1319 1320 # the following options are not documented in usage, because they don't 1321 # apply to most users 1322 #if args.dlv is not None: 1323 # dlv_domain = args.dlv 1324 #else: 1325 # dlv_domain = None 1326 #try: 1327 # cache_level = int(opts['-C']) 1328 #except (KeyError, ValueError): 1329 # cache_level = None 1330 self.dlv_domain = None 1331 self.cache_level = None 1332 self.meta_only = None 1333 1334 if self.args.client_subnet: 1335 CustomQueryMixin.edns_options.append(self.args.client_subnet) 1336 if self.args.nsid: 1337 CustomQueryMixin.edns_options.append(self.args.nsid) 1338 if self.args.cookie: 1339 CustomQueryMixin.edns_options.append(self.args.cookie) 1340 1341 def set_buffers(self): 1342 # This entire method is for 1343 # python3/python2 dual compatibility 1344 if self.args.input_file is not None: 1345 if self.args.input_file.fileno() == sys.stdin.fileno(): 1346 filename = self.args.input_file.fileno() 1347 else: 1348 filename = self.args.input_file.name 1349 self.args.input_file.close() 1350 self.args.input_file = io.open(filename, 'r', encoding='utf-8') 1351 if self.args.names_file is not None: 1352 if self.args.names_file.fileno() == sys.stdin.fileno(): 1353 filename = self.args.names_file.fileno() 1354 else: 1355 filename = self.args.names_file.name 1356 self.args.names_file.close() 1357 self.args.names_file = io.open(filename, 'r', encoding='utf-8') 1358 if self.args.output_file is not None: 1359 if self.args.output_file.fileno() == sys.stdout.fileno(): 1360 filename = self.args.output_file.fileno() 1361 else: 1362 filename = self.args.output_file.name 1363 self.args.output_file.close() 1364 self.args.output_file = io.open(filename, 'wb') 1365 1366 def check_network_connectivity(self): 1367 if self.args.authoritative_analysis: 1368 if self.try_ipv4 and get_client_address(A_ROOT_IPV4) is None: 1369 self._logger.warning('No global IPv4 connectivity detected') 1370 if self.try_ipv6 and get_client_address(A_ROOT_IPV6) is None: 1371 self._logger.warning('No global IPv6 connectivity detected') 1372 1373 def get_log_level(self): 1374 if self.args.debug > 2: 1375 return logging.DEBUG 1376 elif self.args.debug > 1: 1377 return logging.INFO 1378 elif self.args.debug > 0: 1379 return logging.WARNING 1380 else: 1381 return logging.ERROR 1382 1383 def ingest_input(self): 1384 if not self.args.input_file: 1385 return 1386 1387 analysis_str = self.args.input_file.read() 1388 if not analysis_str: 1389 if self.args.input_file.fileno() != sys.stdin.fileno(): 1390 raise AnalysisInputError('No input') 1391 else: 1392 raise AnalysisInputError() 1393 try: 1394 self.analysis_structured = json.loads(analysis_str) 1395 except ValueError: 1396 raise AnalysisInputError('There was an error parsing the JSON input: "%s"' % self.args.input_file.name) 1397 1398 # check version 1399 if '_meta._dnsviz.' not in self.analysis_structured or 'version' not in self.analysis_structured['_meta._dnsviz.']: 1400 raise AnalysisInputError('No version information in JSON input: "%s"' % self.args.input_file.name) 1401 try: 1402 major_vers, minor_vers = [int(x) for x in str(self.analysis_structured['_meta._dnsviz.']['version']).split('.', 1)] 1403 except ValueError: 1404 raise AnalysisInputError('Version of JSON input is invalid: %s' % self.analysis_structured['_meta._dnsviz.']['version']) 1405 # ensure major version is a match and minor version is no greater 1406 # than the current minor version 1407 curr_major_vers, curr_minor_vers = [int(x) for x in str(DNS_RAW_VERSION).split('.', 1)] 1408 if major_vers != curr_major_vers or minor_vers > curr_minor_vers: 1409 raise AnalysisInputError('Version %d.%d of JSON input is incompatible with this software.' % (major_vers, minor_vers)) 1410 1411 def ingest_names(self): 1412 self.names = OrderedDict() 1413 1414 if self.args.domain_name: 1415 for name in self.args.domain_name: 1416 if name not in self.names: 1417 self.names[name] = None 1418 return 1419 1420 if self.args.names_file: 1421 args = self.args.names_file 1422 else: 1423 try: 1424 args = self.analysis_structured['_meta._dnsviz.']['names'] 1425 except KeyError: 1426 raise AnalysisInputError('No names found in JSON input!') 1427 1428 for arg in args: 1429 name = arg.strip() 1430 1431 # python3/python2 dual compatibility 1432 if hasattr(name, 'decode'): 1433 name = name.decode('utf-8') 1434 1435 try: 1436 name = dns.name.from_text(name) 1437 except UnicodeDecodeError as e: 1438 self._logger.error('%s: "%s"' % (e, name)) 1439 except dns.exception.DNSException: 1440 self._logger.error('The domain name was invalid: "%s"' % name) 1441 else: 1442 if name not in self.names: 1443 self.names[name] = None 1444 1445 def serve_zones(self): 1446 for zone in self._zones_to_serve: 1447 zone.serve() 1448 1449def build_helper(logger, cmd, subcmd): 1450 try: 1451 resolver = Resolver.from_file(RESOLV_CONF, StandardRecursiveQueryCD, transport_manager=tm) 1452 except ResolvConfError: 1453 sys.stderr.write('File %s not found or contains no nameserver entries.\n' % RESOLV_CONF) 1454 sys.exit(1) 1455 1456 arghelper = ArgHelper(resolver, logger) 1457 arghelper.build_parser('%s %s' % (cmd, subcmd)) 1458 return arghelper 1459 1460def main(argv): 1461 global tm 1462 global th_factories 1463 global explicit_delegations 1464 global odd_ports 1465 1466 try: 1467 _init_tm() 1468 arghelper = build_helper(logger, sys.argv[0], argv[0]) 1469 arghelper.parse_args(argv[1:]) 1470 logger.setLevel(arghelper.get_log_level()) 1471 1472 try: 1473 arghelper.check_args() 1474 arghelper.set_kwargs() 1475 arghelper.set_buffers() 1476 arghelper.check_network_connectivity() 1477 arghelper.aggregate_delegation_info() 1478 arghelper.populate_recursive_servers() 1479 arghelper.ingest_input() 1480 arghelper.ingest_names() 1481 arghelper.serve_zones() 1482 except argparse.ArgumentTypeError as e: 1483 arghelper.parser.error(str(e)) 1484 except (ZoneFileServiceError, MissingExecutablesError) as e: 1485 s = str(e) 1486 if s: 1487 logger.error(s) 1488 sys.exit(1) 1489 except AnalysisInputError as e: 1490 s = str(e) 1491 if s: 1492 logger.error(s) 1493 sys.exit(3) 1494 1495 th_factories = arghelper.th_factories 1496 explicit_delegations = arghelper.explicit_delegations 1497 odd_ports = arghelper.odd_ports 1498 1499 if arghelper.args.authoritative_analysis: 1500 if arghelper.args.threads > 1: 1501 cls = ParallelAnalyst 1502 else: 1503 cls = BulkAnalyst 1504 else: 1505 if arghelper.args.threads > 1: 1506 cls = RecursiveParallelAnalyst 1507 else: 1508 cls = RecursiveBulkAnalyst 1509 1510 if arghelper.args.pretty_output: 1511 kwargs = { 'indent': 4, 'separators': (',', ': ') } 1512 else: 1513 kwargs = {} 1514 dnsviz_meta = { 'version': DNS_RAW_VERSION, 'names': [lb2s(n.to_text()) for n in arghelper.names] } 1515 1516 name_objs = [] 1517 if arghelper.args.input_file: 1518 cache = {} 1519 for name in arghelper.names: 1520 if name.canonicalize().to_text() not in arghelper.analysis_structured: 1521 logger.error('The domain name was not found in the analysis input: "%s"' % name.to_text()) 1522 continue 1523 name_objs.append(OnlineDomainNameAnalysis.deserialize(name, arghelper.analysis_structured, cache)) 1524 else: 1525 if arghelper.args.threads > 1: 1526 a = cls(arghelper.rdclass, arghelper.try_ipv4, arghelper.try_ipv6, arghelper.client_ipv4, arghelper.client_ipv6, CustomQueryMixin, arghelper.ceiling, arghelper.args.edns, arghelper.stop_at, arghelper.cache_level, arghelper.args.rr_types, arghelper.explicit_only, arghelper.dlv_domain, arghelper.args.threads) 1527 else: 1528 if cls.use_full_resolver: 1529 _init_full_resolver() 1530 else: 1531 _init_stub_resolver() 1532 a = cls(arghelper.rdclass, arghelper.try_ipv4, arghelper.try_ipv6, arghelper.client_ipv4, arghelper.client_ipv6, CustomQueryMixin, arghelper.ceiling, arghelper.args.edns, arghelper.stop_at, arghelper.cache_level, arghelper.args.rr_types, arghelper.explicit_only, arghelper.dlv_domain) 1533 1534 name_objs = a.analyze(arghelper.names) 1535 1536 name_objs = [x for x in name_objs if x is not None] 1537 1538 if not name_objs: 1539 sys.exit(4) 1540 1541 d = OrderedDict() 1542 for name_obj in name_objs: 1543 name_obj.serialize(d, arghelper.meta_only) 1544 d['_meta._dnsviz.'] = dnsviz_meta 1545 1546 try: 1547 arghelper.args.output_file.write(json.dumps(d, ensure_ascii=False, **kwargs).encode('utf-8')) 1548 except IOError as e: 1549 logger.error('Error writing analysis: %s' % e) 1550 sys.exit(3) 1551 1552 except KeyboardInterrupt: 1553 logger.error('Interrupted.') 1554 sys.exit(4) 1555 1556 # tm is global (because of possible multiprocessing), so we need to 1557 # explicitly close it here 1558 finally: 1559 _cleanup_tm() 1560 1561if __name__ == "__main__": 1562 main(sys.argv) 1563