@@ -418,49 +418,83 @@ def exchange_with_tc_fallback(server_ip, wire_query, timeout=2, port=53):
418418# -----------------------------
419419# Iterative resolution (with bailiwick glue)
420420# -----------------------------
421- def iterative_resolve (qname , qtype , timeout = 2 , max_steps = 25 , edns_size = None , do = False ):
421+ def iterative_resolve (qname , qtype , timeout = 2 , max_steps = 25 , edns_size = None , do = False , overall_timeout = 4.0 ):
422422 """
423423 Returns a DNS response message (bytes) from the last server contacted.
424424 Follows referrals iteratively from the root.
425+
426+ Changes:
427+ - Enforces an overall deadline (overall_timeout seconds total)
428+ - Stops early on terminal responses:
429+ * RCODE != 0 (e.g., NXDOMAIN)
430+ * SOA in authority with no answers (NODATA / negative)
431+ - For resolving NS hostnames, tries A before AAAA (IPv4 first)
432+ - Shares overall timeout into recursive NS-name lookups (prevents runaway time)
425433 """
434+ deadline = time .time () + float (overall_timeout )
435+
426436 next_servers = list (ROOT_SERVERS )
427437 random .shuffle (next_servers )
428438 last_msg = None
429439
430440 for _step in range (max_steps ):
431441 if not next_servers :
432442 break
443+ if time .time () >= deadline :
444+ break
433445
434446 server = next_servers .pop (0 )
435447
436448 _tid , wire = build_query (qname , qtype , rd = False , edns_size = edns_size , do = do )
449+
450+ # cap per-try timeout by remaining budget
451+ remaining = deadline - time .time ()
452+ if remaining <= 0 :
453+ break
454+ per_try = min (float (timeout ), max (0.05 , remaining ))
455+
437456 try :
438- resp = exchange_with_tc_fallback (server , wire , timeout = timeout , port = 53 )
457+ resp = exchange_with_tc_fallback (server , wire , timeout = per_try , port = 53 )
439458 except Exception :
440459 continue
441460
442461 last_msg = resp
443- parsed = parse_sections (resp )
444462
445- # If got answers, done
463+ try :
464+ parsed = parse_sections (resp )
465+ except Exception :
466+ # if parsing fails, try next server
467+ continue
468+
469+ # ---- Terminal conditions ----
470+ # Any upstream error (NXDOMAIN, SERVFAIL, etc.)
471+ if parsed ["rcode" ] != 0 :
472+ return resp
473+
474+ # Got direct answers
446475 if parsed ["answers" ]:
447476 return resp
448477
449- # Referral: authority NS + additional glue
478+ # Authoritative negative (NODATA): SOA in authority
479+ if any (rr ["type" ] == QTYPE ["SOA" ] for rr in parsed ["authority" ]):
480+ return resp
481+
482+ # ---- Referral processing: authority NS + additional glue ----
450483 ns_names = []
451- bailiwick_zone = None
452484 for rr in parsed ["authority" ]:
453485 if rr ["type" ] == QTYPE ["NS" ]:
454- bailiwick_zone = rr ["name" ] # delegation zone
455486 nsn , _ = decode_name (resp , rr ["rdata_off" ])
456- ns_names .append (nsn )
487+ ns_names .append (_dnsname_norm ( nsn ) )
457488
489+ # Prefer glue that matches the referred NS hostnames (even if out-of-bailiwick)
458490 glue_ips = []
459- if bailiwick_zone :
491+ if ns_names :
492+ ns_set = set (ns_names )
460493 for rr in parsed ["additional" ]:
461494 if rr ["type" ] not in (QTYPE ["A" ], QTYPE ["AAAA" ]):
462495 continue
463- if not _is_in_bailiwick (rr .get ("name" ), bailiwick_zone ):
496+ owner = _dnsname_norm (rr .get ("name" ))
497+ if owner not in ns_set :
464498 continue
465499 ip = rr_ip_from_additional (rr )
466500 if ip :
@@ -471,29 +505,52 @@ def iterative_resolve(qname, qtype, timeout=2, max_steps=25, edns_size=None, do=
471505 next_servers = glue_ips + next_servers
472506 continue
473507
474- # No usable glue: resolve NS hostnames (AAAA first, then A)
508+ # ---- No usable glue: resolve NS hostnames ----
475509 if ns_names :
476510 random .shuffle (ns_names )
477511 resolved_ns_ips = []
478512
513+ # try a few NS names
479514 for nsn in ns_names [:3 ]:
480515 resolved_ns_ips = []
481- for qt in (QTYPE ["AAAA" ], QTYPE ["A" ]):
516+
517+ # IMPORTANT CHANGE: prefer IPv4 first, then IPv6
518+ for qt in (QTYPE ["A" ], QTYPE ["AAAA" ]):
519+ # Keep NS-name lookups cheaper + share overall budget
520+ remaining = deadline - time .time ()
521+ if remaining <= 0 :
522+ break
523+
482524 ns_resp = iterative_resolve (
483- nsn , qt , timeout = timeout , max_steps = max_steps ,
484- edns_size = edns_size , do = do
525+ nsn , qt ,
526+ timeout = min (float (timeout ), 0.5 ),
527+ max_steps = 10 ,
528+ edns_size = edns_size ,
529+ do = do ,
530+ overall_timeout = max (0.1 , remaining )
485531 )
486532 if not ns_resp :
487533 continue
488- ns_parsed = parse_sections (ns_resp )
534+
535+ try :
536+ ns_parsed = parse_sections (ns_resp )
537+ except Exception :
538+ continue
539+
540+ # If NS-name lookup got an error, ignore and try next
541+ if ns_parsed ["rcode" ] != 0 :
542+ continue
543+
489544 for rr in ns_parsed ["answers" ]:
490545 if rr ["type" ] == QTYPE ["A" ] and rr ["rdlen" ] == 4 :
491546 b = bytearray (rr ["rdata" ])
492547 resolved_ns_ips .append ("%d.%d.%d.%d" % (b [0 ], b [1 ], b [2 ], b [3 ]))
493548 elif rr ["type" ] == QTYPE ["AAAA" ] and rr ["rdlen" ] == 16 :
494549 resolved_ns_ips .append (socket .inet_ntop (socket .AF_INET6 , rr ["rdata" ]))
550+
495551 if resolved_ns_ips :
496552 break
553+
497554 if resolved_ns_ips :
498555 break
499556
@@ -502,6 +559,7 @@ def iterative_resolve(qname, qtype, timeout=2, max_steps=25, edns_size=None, do=
502559 next_servers = resolved_ns_ips + next_servers
503560 continue
504561
562+ # nothing else to try; return what we got
505563 return resp
506564
507565 return last_msg
@@ -635,20 +693,45 @@ def min_ttl_from_answers(resp_msg):
635693 return 30
636694
637695
696+ def _query_question_wire (query_msg ):
697+ """
698+ Return the raw Question section (QNAME/QTYPE/QCLASS) from a client QUERY message.
699+ This excludes any EDNS OPT or other additional records that may be present.
700+ """
701+ _tid , _flags , qd , _an , _ns , _ar , _tc , _rcode = parse_header (query_msg )
702+ off = 12
703+ off2 = skip_questions (query_msg , off , qd )
704+ return query_msg [12 :off2 ]
705+
706+
638707def make_servfail (query_wire ):
708+ """
709+ Return a minimal SERVFAIL response that echoes the Question section only.
710+ Avoids dig's 'extra bytes at end' warning.
711+ """
639712 if len (query_wire ) < 12 :
713+ # minimal fallback (rare)
640714 return b"\x00 \x00 " + struct .pack ("!H" , 0x8002 ) + b"\x00 \x01 \x00 \x00 \x00 \x00 \x00 \x00 "
715+
641716 tid = query_wire [:2 ]
642- hdr = tid + struct .pack ("!H" , 0x8002 ) + struct .pack ("!HHHH" , 1 , 0 , 0 , 0 )
643- return hdr + query_wire [12 :]
717+ flags = 0x8000 | 0x0002 # QR=1, RCODE=2 (SERVFAIL)
718+ qwire = _query_question_wire (query_wire )
719+ hdr = tid + struct .pack ("!H" , flags ) + struct .pack ("!HHHH" , 1 , 0 , 0 , 0 )
720+ return hdr + qwire
644721
645722
646723def make_nxdomain (query_wire ):
724+ """
725+ Return a minimal NXDOMAIN response that echoes the Question section only.
726+ """
647727 if len (query_wire ) < 12 :
648728 return b"\x00 \x00 " + struct .pack ("!H" , 0x8003 ) + b"\x00 \x01 \x00 \x00 \x00 \x00 \x00 \x00 "
729+
649730 tid = query_wire [:2 ]
650- hdr = tid + struct .pack ("!H" , 0x8003 ) + struct .pack ("!HHHH" , 1 , 0 , 0 , 0 )
651- return hdr + query_wire [12 :]
731+ flags = 0x8000 | 0x0003 # QR=1, RCODE=3 (NXDOMAIN)
732+ qwire = _query_question_wire (query_wire )
733+ hdr = tid + struct .pack ("!H" , flags ) + struct .pack ("!HHHH" , 1 , 0 , 0 , 0 )
734+ return hdr + qwire
652735
653736
654737def _rewrite_response_for_client (resp , client_tid_bytes , client_query_flags ):
0 commit comments