File: blaeu-resolve

package info (click to toggle)
blaeu 2.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 272 kB
  • sloc: python: 2,014; makefile: 3
file content (470 lines) | stat: -rwxr-xr-x 22,144 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

""" Python code to start a RIPE Atlas UDM (User-Defined
Measurement). This one is for running DNS to resolve a name from many
places, in order to survey local cache poisonings, effect of
hijackings and other DNS rejuvenation effects.

You'll need an API key in ~/.atlas/auth.

After launching the measurement, it downloads the results and analyzes
them.

Stéphane Bortzmeyer <stephane+frama@bortzmeyer.org>
"""

import json
import sys
import time
import base64
import copy
import collections

# DNS Python http://www.dnspython.org/
import dns.message

import Blaeu
from Blaeu import Host_Type

config = Blaeu.Config()
# Default values
config.qtype = 'AAAA'
config.qclass = "IN"
config.display_resolvers = False
config.display_rtt = False
config.display_validation = False
config.edns_size = None
config.dnssec = False
config.dnssec_checking = True
config.nameserver = None
config.recursive = True
config.sort = False
config.nsid = False
config.ede = False
config.only_one_per_probe = True
config.protocol = "UDP"
config.tls = False
config.probe_id = False
config.answer_section = True
config.authority_section = False
config.additional_section = False

# Local values
edns_size = None

# Constants
MAXLEN = 80 # Maximum length of a displayed resource record

class Set():
    def __init__(self):
        self.total = 0
        self.successes = 0
        self.rtt = 0

def usage(msg=None):
    print("Usage: %s domain-name" % sys.argv[0], file=sys.stderr)
    config.usage(msg)
    print("""Also:
    --displayresolvers or -l : display the resolvers IP addresses (WARNING: big lists)
    --norecursive or -Z : asks the resolver to NOT recurse (default is to recurse, note --norecursive works ONLY if asking a specific resolver, not with the default one)
    --dnssec or -D : asks the resolver the DNSSEC records
    --nsid : asks the resolver with NSID (name server identification)
    --ede : displays EDE (Extended DNS Errors)
    --ednssize=N or -B N : asks for EDNS with the "payload size" option (default is very old DNS, without EDNS)
    --tcp: uses TCP (default is UDP)
    --tls: uses TLS (implies TCP)
    --checkingdisabled or -k : asks the resolver to NOT perform DNSSEC validation
    --displayvalidation or -j : displays the DNSSEC validation status
    --displayrtt : displays the average RTT
    --authority : displays the Authority section of the answer
    --additional : displays the Additional section of the answer 
    --sort or -S : sort the result sets
    --type or -q : query type (default is %s)
    --class : query class (default is %s)
    --severalperprobe : count all the resolvers of each probe (default is to count only the first to reply)
    --nameserver=name_or_IPaddr[,...] or -x name_or_IPaddr : query this name server (default is to query the probe's resolver)
    --probe_id : prepend probe ID (and timestamp) to the domain name (default is to abstain)
    """ % (config.qtype, config.qclass), file=sys.stderr)

def specificParse(config, option, value):
        result = True
        if option == "--type" or option == "-q":
            config.qtype = value
        elif option == "--class": # For Chaos, use "CHAOS", not "CH"
            config.qclass = value
        elif option == "--norecursive" or option == "-Z":
            config.recursive = False
        elif option == "--dnssec" or option == "-D":
            config.dnssec = True
        elif option == "--nsid":
            config.nsid = True
        elif option == "--ede":
            config.ede = True
        elif option == "--probe_id":
            config.probe_id = True
        elif option == "--ednssize" or option == "-B":
            config.edns_size = int(value)
        elif option == "--tcp":
            config.protocol = "TCP"
        elif option == "--tls":
            config.tls = True
        elif option == "--checkingdisabled" or option == "-k":
            config.dnssec_checking = False
        elif option == "--sort" or option == "-S":
            config.sort = True
        elif option == "--authority":
            config.answer_section = False
            config.authority_section = True
        elif option == "--additional":
            config.answer_section = False
            config.additional_section = True
        elif option == "--nameserver" or option == "-x":
            config.nameserver = value
            config.nameservers = config.nameserver.split(",")
        elif option == "--displayresolvers" or option == "-l":
            config.display_resolvers = True
        elif option == "--displayvalidation" or option == "-j":
            config.display_validation = True
        elif option == "--displayrtt":
            config.display_rtt = True
        elif option == "--severalperprobe":
            config.only_one_per_probe = False
        else:
            result = False
        return result
    
args, data = config.parse("q:ZDkSx:ljB:", ["type=", "class=", "ednssize=",
                                     "displayresolvers", "probe_id",
                                     "displayrtt", "displayvalidation",
                                     "dnssec", "nsid", "ede", "norecursive",
                                     "authority", "additional",
                                     "tcp", "tls", "checkingdisabled",
                                     "nameserver=", "sort",
                                     "severalperprobe"], specificParse,
                    usage)

if len(args) != 1:
    usage()
    sys.exit(1)

domainname = args[0]

if config.tls:
    config.protocol = "TCP"
    # We don't set the port (853) but Atlas does it for us
    
data["definitions"][0]["type"] = "dns"
del data["definitions"][0]["size"]
del data["definitions"][0]["port"]
data["definitions"][0]["query_argument"] = domainname
data["definitions"][0]["description"] = ("DNS resolution of %s/%s" % (domainname, config.qtype)) + data["definitions"][0]["description"]
data["definitions"][0]["query_class"] = config.qclass
data["definitions"][0]["query_type"] = config.qtype
if config.edns_size is not None and config.protocol == "UDP":
    data["definitions"][0]["udp_payload_size"] = config.edns_size
    edns_size = config.edns_size
if config.dnssec or config.display_validation: # https://atlas.ripe.net/docs/api/v2/reference/#!/measurements/Dns_Type_Measurement_List_POST
    data["definitions"][0]["set_do_bit"] = True
    if config.edns_size is None and config.protocol == "UDP":
        edns_size = 4096
if config.nsid: 
    data["definitions"][0]["set_nsid_bit"] = True
    if config.edns_size is None and config.protocol == "UDP":
        edns_size = 1024
if config.ede: 
    if config.edns_size is None and config.protocol == "UDP":
        edns_size = 1024
if edns_size is not None and config.protocol == "UDP":
    data["definitions"][0]["udp_payload_size"] = edns_size
if not config.dnssec_checking:
    data["definitions"][0]["set_cd_bit"] = True
if config.recursive:
    data["definitions"][0]["set_rd_bit"] = True
else:
    data["definitions"][0]["set_rd_bit"] = False
if config.tls:
    data["definitions"][0]["tls"] = True
if config.probe_id:
    data["definitions"][0]["prepend_probe_id"] = True
data["definitions"][0]["protocol"] = config.protocol
if config.verbose and config.machine_readable:
    usage("Specify verbose *or* machine-readable output")
    sys.exit(1)
if (config.display_probes or config.display_probe_asns or config.display_resolvers or config.display_rtt) and config.machine_readable:
    usage("Display probes/probeasns/resolvers/RTT *or* machine-readable output")
    sys.exit(1)

if config.nameserver is None:
    config.nameservers = [None,]

description = data["definitions"][0]["description"]
for nameserver in config.nameservers:
    if nameserver is None:
        data["definitions"][0]["use_probe_resolver"] = True
        # Exclude probes which do not have at least one working resolver
        data["probes"][0]["tags"]["include"].append("system-resolves-a-correctly")
        data["probes"][0]["tags"]["include"].append("system-resolves-aaaa-correctly")
    else:
        data["definitions"][0]["use_probe_resolver"] = False
        data["definitions"][0]["target"] = nameserver
        serveraddr_type = Blaeu.host_type(nameserver)
        data["definitions"][0]["description"] = description + (" via nameserver %s" % nameserver)
        if serveraddr_type == Host_Type.IPv6:
            config.ipv4 = False
            data["definitions"][0]['af'] = 6 
            if config.include is not None:
                data["probes"][0]["tags"]["include"] = copy.copy(config.include)
                data["probes"][0]["tags"]["include"].append("system-ipv6-works")
            else:
                data["probes"][0]["tags"]["include"] = ["system-ipv6-works",]
        elif serveraddr_type == Host_Type.IPv4:
            config.ipv4 = True
            data["definitions"][0]['af'] = 4
            if config.include is not None:
                data["probes"][0]["tags"]["include"] = copy.copy(config.include)
                data["probes"][0]["tags"]["include"].append("system-ipv4-works")
            else:
                data["probes"][0]["tags"]["include"] = ["system-ipv4-works",]
        else: # Probably an host name
            pass
    if config.measurement_id is None:
        if config.verbose:
            print(data)
        try:
            measurement = Blaeu.Measurement(data,
                                        lambda delay: sys.stderr.write(
                "Sleeping %i seconds...\n" % delay))
        except Blaeu.RequestSubmissionError as error:
            print(Blaeu.format_error(error), file=sys.stderr)
            sys.exit(1)
        if not config.machine_readable and config.verbose:
            print("Measurement #%s for %s/%s uses %i probes" % \
            (measurement.id, domainname, config.qtype, measurement.num_probes))

        old_measurement = measurement.id
        results = measurement.results(wait=True)
    else:
        measurement = Blaeu.Measurement(data=None, id=config.measurement_id)
        results = measurement.results(wait=False)
        if config.verbose:
            print("%i results from already-done measurement %s" % (len(results), measurement.id))
    if len(results) == 0:
        print("Warning: zero results. Measurement not terminated? May be retry later with --measurement-ID=%s ?" % (measurement.id), file=sys.stderr)
    probes = 0
    successes = 0

    qtype_num = dns.rdatatype.from_text(config.qtype) # Raises dns.rdatatype.UnknownRdatatype if unknown
    sets = collections.defaultdict(Set)
    if config.display_probes:
        probes_sets = collections.defaultdict(set)
    if config.display_probe_asns:
        probe_asns_sets = collections.defaultdict(set)
    if config.display_resolvers:
        resolvers_sets = collections.defaultdict(set)
    for result in results:
        probes += 1
        probe_id = result["prb_id"]
        if config.display_probe_asns:
            _probe = Blaeu.ProbeCache.cache_probe_id(config.cache_probes, probe_id) if config.cache_probes else Blaeu.Probe(probe_id)
        first_error = ""
        probe_resolves = False
        resolver_responds = False
        all_timeout = True
        if "result" in result:
            result_set = [{'result': result['result']},]
        elif "resultset" in result:
            result_set = result['resultset']
        elif "error" in result:
            result_set = []
            myset = []
            if "timeout" in result['error']:
                myset.append("TIMEOUT")
            elif "socket" in result['error']:
                all_timeout = False
                myset.append("NETWORK PROBLEM WITH RESOLVER")
            elif "TUCONNECT" in result['error']:
                all_timeout = False
                myset.append("TUCONNECT (may be a TLS negotiation error or a TCP connection issue)")
            else:
                all_timeout = False
                myset.append("NO RESPONSE FOR UNKNOWN REASON at probe %s" % probe_id)
        else:
            raise Blaeu.WrongAssumption("Neither result not resultset member")
        if len(result_set) == 0:
            myset.sort()
            set_str = " ".join(myset)
            sets[set_str].total += 1
            if config.display_probes:
                probes_sets[set_str].add(probe_id)
            if config.display_probe_asns:
                probe_asns_sets[set_str].add(getattr(_probe, "asn_v%i" % (4 if config.ipv4 else 6), None))
        for result_i in result_set:
            try:
                if "dst_addr" in result_i:
                    resolver = str(result_i['dst_addr'])
                elif "dst_name" in result_i: # Apparently, used when there was a problem
                    resolver = str(result_i['dst_name'])
                elif "dst_addr" in result: # Used when specifying a name server
                    resolver = str(result['dst_addr'])
                elif "dst_name" in result: # Apparently, used when there was a problem
                    resolver = str(result['dst_name'])
                else:
                    resolver = "UNKNOWN RESOLUTION ERROR"
                myset = []
                if "result" not in result_i:
                    if config.only_one_per_probe:
                        continue
                    else:
                        if "timeout" in result_i['error']:
                            myset.append("TIMEOUT")
                        elif "socket" in result_i['error']:
                            all_timeout = False
                            myset.append("NETWORK PROBLEM WITH RESOLVER")
                        else:
                            all_timeout = False
                            myset.append("NO RESPONSE FOR UNKNOWN REASON at probe %s" % probe_id)
                else:
                    all_timeout = False
                    resolver_responds = True
                    answer = result_i['result']['abuf'] + "=="
                    content = base64.b64decode(answer)
                    msg = dns.message.from_wire(content)
                    if config.ede:
                        if hasattr(dns.edns, 'EDE'): # Appeared in DNS Python in 2024?
                            opt_ede = next((opt for opt in msg.options if opt.otype == dns.edns.EDE), None)
                            if opt_ede:
                                ede = opt_ede.to_text()
                                myset.append(ede)
                    if config.nsid:
                        opt_nsid = next((opt for opt in msg.options if opt.otype == dns.edns.NSID), None)
                        # dnspython handles NSID with version >=
                        # 2.6. Before that, we have to do it.
                        if opt_nsid and hasattr(opt_nsid, "data"): # < 2.6
                            nsid = opt_nsid.data.decode()
                        elif opt_nsid and hasattr(opt_nsid, "nsid"): # > 2.6
                            nsid = opt_nsid.to_text()
                            if nsid.startswith("NSID "):
                                nsid = nsid[5:] # Remove the "NSID " prefix
                        else: # NSID option not found or could not be parsed
                            nsid = None
                        myset.append("NSID: %s;" % nsid)
                    successes += 1
                    if msg.rcode() == dns.rcode.NOERROR:
                        probe_resolves = True
                        if config.answer_section:
                            if result_i['result']['ANCOUNT'] == 0 and config.verbose:
                                # If we test an authoritative server, and it returns a delegation, we won't see anything...
                                print("Warning: reply at probe %s has no answers: may be the server returned a delegation, or does not have data of type %s? For the first case, you may want to use --authority." % (probe_id, config.qtype), file=sys.stderr)
                            interesting_section = msg.answer
                        elif config.authority_section:
                            interesting_section = msg.authority
                        elif config.additional_section:
                            interesting_section = msg.additional
                        for rrset in interesting_section:
                            for rdata in rrset:
                                if rdata.rdtype == qtype_num:
                                    myset.append(str(rdata)[0:MAXLEN].lower()) # We truncate because DNSKEY can be very long
                        if config.display_validation and (msg.flags & dns.flags.AD):
                            myset.append(" (Authentic Data flag) ")
                        if (msg.flags & dns.flags.TC):
                            if edns_size is not None:
                                myset.append(" (TRUNCATED - EDNS buffer size was %d ) " % edns_size)
                            else:
                                myset.append(" (TRUNCATED - May have to use --ednssize) ")
                    else:
                        if msg.rcode() == dns.rcode.REFUSED: # Not SERVFAIL since
                            # it can be legitimate (DNSSEC problem, for instance)
                            if config.only_one_per_probe and len(result_set) > 1: # It
                                # does not handle the case where there
                                # are several resolvers and all say
                                # REFUSED (probably a rare case).
                                if first_error == "":
                                    first_error = "ERROR: %s" % dns.rcode.to_text(msg.rcode())
                                continue # Try again
                        else:
                            probe_resolves = True # NXDOMAIN or SERVFAIL are legitimate
                        myset.append("ERROR: %s" % dns.rcode.to_text(msg.rcode()))
                myset.sort() # Not ideal since the alphabetical sort
                             # will, for instance, put EDE before
                             # ERROR. We should create myset more
                             # intelligently.
                set_str = " ".join(myset)
                sets[set_str].total += 1
                if "error" not in result_i:
                    sets[set_str].successes += 1
                if config.display_probes:
                    probes_sets[set_str].add(probe_id)
                if config.display_probe_asns:
                    _res_af = result_i.get("af") or result.get("af")
                    if _res_af:
                        probe_asns_sets[set_str].add(getattr(_probe, "asn_v%i" % _res_af))
                if config.display_resolvers:
                    resolvers_sets[set_str].add(resolver)
                if config.display_rtt:
                    if "error" not in result_i:
                        if "result" not in result_i:
                            sets[set_str].rtt +=  result_i['rt']
                        else:
                            sets[set_str].rtt +=  result_i['result']['rt']
            except dns.name.BadLabelType:
                if not config.machine_readable:
                    print("Probe %s failed (bad label in name)" % probe_id, file=sys.stderr)
            except dns.message.TrailingJunk:
                if not config.machine_readable:
                    print("Probe %s failed (trailing junk)" % probe_id, file=sys.stderr)
            except dns.exception.FormError:
                if not config.machine_readable:
                    print("Probe %s failed (malformed DNS message)" % probe_id, file=sys.stderr)
            if config.only_one_per_probe:
                    break
        if not probe_resolves and first_error != "" and config.verbose:
            print("Warning, probe %s has no working resolver (first error is \"%s\")" % (probe_id, first_error), file=sys.stderr)
        if not resolver_responds:
            if all_timeout and not config.only_one_per_probe:
                if config.verbose:
                    print("Warning, probe %s never got reply from any resolver" % (probe_id), file=sys.stderr)
                set_str = "TIMEOUT(S) on all resolvers"
                sets[set_str].total += 1
            else:
                myset.sort()
                set_str = " ".join(myset)
    if config.sort:
        sets_data = sorted(sets, key=lambda s: sets[s].total, reverse=True)
    else:
        sets_data = sets
    details = []
    if not config.machine_readable and config.nameserver is not None:
            print("Nameserver %s" % nameserver)
    if not config.answer_section:
        if config.authority_section:
            print("Authority section of the DNS responses")
        elif config.additional_section:
            print("Additional section of the DNS responses")
        else:
            print("INTERNAL PROBLEM: no section to display?")
    for myset in sets_data:
        detail = ""
        if config.display_probes:
            detail = "(probes %s)" % probes_sets[myset]
        if config.display_probe_asns:
            detail = "(probe asns %s)" % probe_asns_sets[myset]
        if config.display_resolvers:
            detail += "(resolvers %s)" % resolvers_sets[myset]
        if config.display_rtt and sets[myset].successes > 0:
            detail += "Average RTT %i ms" % (sets[myset].rtt/sets[myset].successes)
        if not config.machine_readable:
            print("[%s] : %i occurrences %s" % (myset, sets[myset].total, detail))
        else:
            details.append("[%s];%i" % (myset, sets[myset].total))

    if not config.machine_readable:
        print(("Test #%s done at %s" % (measurement.id, time.strftime("%Y-%m-%dT%H:%M:%SZ", measurement.time))))
        print("")
    else:
        if config.nameserver is None:
            ns = "DEFAULT RESOLVER"
        else:
            ns = config.nameserver
        print(",".join([domainname, config.qtype, str(measurement.id), "%s/%s" % (len(results), measurement.num_probes), \
                        time.strftime("%Y-%m-%dT%H:%M:%SZ", measurement.time), ns] + details))