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