Skip to content

Commit 3ac601c

Browse files
authored
Add files via upload
1 parent 4a3f9a7 commit 3ac601c

1 file changed

Lines changed: 147 additions & 29 deletions

File tree

dns.py

Lines changed: 147 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
36131
def encode_qname(domain):
37132
"""
38133
Encode a domain name into DNS QNAME format:
@@ -325,36 +420,45 @@ def _recvn(sock, n):
325420

326421

327422
def 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

338439
def 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

360464
def 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

Comments
 (0)