@@ -33,6 +33,101 @@ def _byte_at(b, i):
3333 return v if isinstance (v , int ) else ord (v )
3434
3535
36+ def _sock_family_for_server (host ):
37+ # If it contains ':' treat as IPv6 literal (including IPv6%zone)
38+ return socket .AF_INET6 if ":" in host else socket .AF_INET
39+
40+ def _split_ipv6_zone (host ):
41+ """
42+ Split IPv6 address and zone-id if present:
43+ 'fe80::1%wlan0' -> ('fe80::1', 'wlan0')
44+ '::1' -> ('::1', None)
45+ """
46+ if "%" in host :
47+ addr , zone = host .split ("%" , 1 )
48+ return addr , zone
49+ return host , None
50+
51+ def _scope_id_from_zone (zone ):
52+ """
53+ Convert zone string to numeric scope id.
54+ - If zone is digits, use it directly.
55+ - Else try socket.if_nametoindex(zone) (Linux/Android usually supports this).
56+ """
57+ if zone is None :
58+ return 0
59+ if zone .isdigit ():
60+ try :
61+ return int (zone )
62+ except Exception :
63+ return 0
64+ if hasattr (socket , "if_nametoindex" ):
65+ try :
66+ return socket .if_nametoindex (zone )
67+ except Exception :
68+ return 0
69+ return 0
70+
71+ def _is_ipv6_literal (s ):
72+ # crude but effective: IPv6 literals contain ':'
73+ return ":" in s
74+
75+ def _addr_tuple (host , port ):
76+ """
77+ Return a sockaddr suitable for sendto/connect:
78+ IPv4: (host, port)
79+ IPv6: (addr, port, flowinfo, scopeid)
80+
81+ Supports IPv6 zone IDs like 'fe80::1%wlan0'.
82+ """
83+ port = int (port )
84+ if _is_ipv6_literal (host ):
85+ addr , zone = _split_ipv6_zone (host )
86+ scopeid = _scope_id_from_zone (zone )
87+ return (addr , port , 0 , scopeid )
88+ return (host , port )
89+
90+ def _iter_server_addrs (dns_server , port ):
91+ """
92+ Yield sockaddr tuples to try, in a good order.
93+ - If dns_server is an IP literal (v4 or v6%zone), just yield that.
94+ - If it's a hostname, resolve with getaddrinfo and yield all candidates.
95+ """
96+ port = int (port )
97+
98+ # IP literal path (IPv6 includes %zone)
99+ if _is_ipv6_literal (dns_server ) or dns_server .replace ("." , "" ).isdigit ():
100+ yield _addr_tuple (dns_server , port )
101+ return
102+
103+ # Hostname path
104+ try :
105+ infos = socket .getaddrinfo (
106+ dns_server ,
107+ port ,
108+ socket .AF_UNSPEC ,
109+ 0 ,
110+ 0 ,
111+ socket .AI_ADDRCONFIG
112+ )
113+ except Exception :
114+ # fallback without AI_ADDRCONFIG
115+ infos = socket .getaddrinfo (dns_server , port , socket .AF_UNSPEC )
116+
117+ # Prefer IPv6 first, then IPv4 (you can flip this if you want)
118+ def _rank (info ):
119+ fam = info [0 ]
120+ return 0 if fam == socket .AF_INET6 else 1
121+
122+ seen = set ()
123+ for fam , socktype , proto , canonname , sockaddr in sorted (infos , key = _rank ):
124+ # sockaddr can be 2-tuple (v4) or 4-tuple (v6)
125+ if sockaddr in seen :
126+ continue
127+ seen .add (sockaddr )
128+ yield sockaddr
129+
130+
36131def encode_qname (domain ):
37132 """
38133 Encode a domain name into DNS QNAME format:
@@ -325,36 +420,45 @@ def _recvn(sock, n):
325420
326421
327422def query_udp (dns_server , query , port , timeout ):
328- sock = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
329- sock .settimeout (float (timeout ))
330- try :
331- sock .sendto (query , (dns_server , int (port )))
332- msg , _ = sock .recvfrom (4096 ) # allow EDNS-size-ish in practice; still fine
333- return msg
334- finally :
335- sock .close ()
423+ last_err = None
424+ for sockaddr in _iter_server_addrs (dns_server , port ):
425+ fam = socket .AF_INET6 if len (sockaddr ) == 4 else socket .AF_INET
426+ sock = socket .socket (fam , socket .SOCK_DGRAM )
427+ sock .settimeout (float (timeout ))
428+ try :
429+ sock .sendto (query , sockaddr )
430+ msg , _ = sock .recvfrom (4096 )
431+ return msg
432+ except Exception as e :
433+ last_err = e
434+ finally :
435+ sock .close ()
436+ raise last_err if last_err else RuntimeError ("UDP query failed for all addresses" )
336437
337438
338439def query_tcp (dns_server , query , port , timeout ):
339- """
340- DNS over TCP: prefix query with 2-byte length; response also length-prefixed.
341- """
342- sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
343- sock .settimeout (float (timeout ))
344- try :
345- sock .connect ((dns_server , int (port )))
346- sock .sendall (struct .pack ("!H" , len (query )) + query )
347-
348- hdr = _recvn (sock , 2 )
349- if hdr is None :
350- raise RuntimeError ("TCP DNS: no length header" )
351- (msg_len ,) = struct .unpack ("!H" , hdr )
352- msg = _recvn (sock , msg_len )
353- if msg is None :
354- raise RuntimeError ("TCP DNS: incomplete message" )
355- return msg
356- finally :
357- sock .close ()
440+ last_err = None
441+ for sockaddr in _iter_server_addrs (dns_server , port ):
442+ fam = socket .AF_INET6 if len (sockaddr ) == 4 else socket .AF_INET
443+ sock = socket .socket (fam , socket .SOCK_STREAM )
444+ sock .settimeout (float (timeout ))
445+ try :
446+ sock .connect (sockaddr )
447+ sock .sendall (struct .pack ("!H" , len (query )) + query )
448+
449+ hdr = _recvn (sock , 2 )
450+ if hdr is None :
451+ raise RuntimeError ("TCP DNS: no length header" )
452+ (msg_len ,) = struct .unpack ("!H" , hdr )
453+ msg = _recvn (sock , msg_len )
454+ if msg is None :
455+ raise RuntimeError ("TCP DNS: incomplete message" )
456+ return msg
457+ except Exception as e :
458+ last_err = e
459+ finally :
460+ sock .close ()
461+ raise last_err if last_err else RuntimeError ("TCP query failed for all addresses" )
358462
359463
360464def decode_dns_message (msg , wanted_type = None , show_all = False ,
@@ -502,7 +606,8 @@ def main():
502606 parser = argparse .ArgumentParser (
503607 description = "DNS Query Script (Python 2/3): A/AAAA/MX/CNAME/NS/TXT, TCP fallback, optional authority/additional"
504608 )
505- parser .add_argument ('--dns-server' , type = str , help = 'DNS server IP address (e.g., 8.8.8.8)' )
609+ parser .add_argument ('--dns-server' , type = str ,
610+ help = 'DNS server address (IPv4, IPv6, IPv6%zone, or hostname; e.g. 8.8.8.8, ::1, fe80::1%wlan0, localhost)' )
506611 parser .add_argument ('--domain' , type = str , help = 'Domain to look up (e.g., example.com)' )
507612 parser .add_argument ('--record-type' , type = str , default = 'A' ,
508613 help = 'DNS record type (A, AAAA, MX, CNAME, NS, TXT)' )
@@ -517,6 +622,10 @@ def main():
517622 parser .add_argument ('--include-additional' , action = 'store_true' ,
518623 help = 'Also parse/show Additional section (ARCOUNT)' )
519624 parser .add_argument ('--quiet' , action = 'store_true' , help = 'Less header/debug output' )
625+ parser .add_argument ('--prefer-ipv4' , action = 'store_true' ,
626+ help = 'Prefer IPv4 addresses first when --dns-server is a hostname' )
627+ parser .add_argument ('--prefer-ipv6' , action = 'store_true' ,
628+ help = 'Prefer IPv6 addresses first when --dns-server is a hostname (default)' )
520629 args = parser .parse_args ()
521630
522631 # Python 2 input compatibility
@@ -531,6 +640,14 @@ def main():
531640
532641 verbose = (not args .quiet )
533642
643+ # Default preference is IPv6 first (unless user prefers IPv4)
644+ prefer_ipv6 = True
645+ if args .prefer_ipv4 :
646+ prefer_ipv6 = False
647+ if args .prefer_ipv6 :
648+ prefer_ipv6 = True
649+
650+
534651 try :
535652 records = perform_dns_query (
536653 dns_server = dns_server ,
@@ -542,7 +659,8 @@ def main():
542659 show_all = args .show_all ,
543660 include_authority = args .include_authority ,
544661 include_additional = args .include_additional ,
545- verbose = verbose
662+ verbose = verbose ,
663+ prefer_ipv6 = prefer_ipv6 ,
546664 )
547665
548666 if records :
0 commit comments