mdns-termux/lib/mdns_tools/reverse.py

94 lines
2.9 KiB
Python
Raw Permalink Normal View History

feat: CLI unificado mdns con browse/reverse/IPv6/cache-TTL Cierra el gap hacia una implementacion mDNS/DNS-SD completa (RFC 6762/6763). CLI nuevo `bin/mdns` con subcomandos: - resolve : hostname -> IP (IPv4 stdlib rapido, IPv6 via zeroconf), cache TTL. - reverse : IP -> hostname (PTR multicast, IPv4/IPv6). - browse : descubrimiento DNS-SD (--list-types o --type _ssh._tcp.local.). - publish : anuncia hostname + servicios (SSH/SMB/HTTP) con probing y goodbye. - cache : list/clear del cache con TTL. Biblioteca lib/mdns_tools/ (cada modulo < 100 lineas): - resolve.py: stdlib-first para IPv4, zeroconf fallback para IPv6/miss. - _stdlib_resolve.py: resolver A puro stdlib, acumula todas las A del host. - _pick.py: elige la IP mas alcanzable ante multi-homing. Descarta rangos virtuales (libvirt 192.168.122/24, docker 172.17/16, link-local) devolviendo None para que el caller caiga a Tailscale en vez de a una IP muerta. - browse.py / publish.py: DNS-SD sobre zeroconf. - reverse.py: PTR stdlib. cache.py: TTL-aware JSON en ~/.cache/mdns. Mejoras de calidad: - IP_ADD_MEMBERSHIP en todos los sockets multicast. - Retries con backoff (timeout dividido). - Imports lazy en el CLI (resolve/cache no cargan zeroconf -> ~1s vs ~4s). Wrappers: - resolve (bash): orden mdns -> mdns-resolve stdlib -> Tailscale -> cache plano. - mdns-publish.py: shim compat sobre `mdns publish`, lee ~/.config/mdns/services.json. - install.sh: symlinks bin/ (updates via git pull), siembra services.json. - ssh_config.example: HostKeyAlias documentado (alias comparte known_hosts con el nombre canonico; evita fallos de verificacion tras reinstalar un host). - bashrc.snippet: aliases mdns-types/ssh/smb/http, mdns-cache, whor. Validado E2E en LAN: browse detecta 13 tipos; dell/movil resuelven a IP LAN real; lenovo (solo publica virbr0 192.168.122.1) cae correctamente a Tailscale; IPv6 de dell via zeroconf; reverse 192.168.1.17 -> dell-latitude3400.local; publish visible desde otro browser. Modulos < 100 lineas salvo el dispatcher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-20 12:06:53 +00:00
"""Reverse mDNS lookup: IP address -> hostname (PTR record on multicast)."""
import ipaddress
import socket
import struct
import select
import time
from ._stdlib_resolve import build_query, skip_name, MDNS_ADDR4, MDNS_PORT
MDNS_ADDR6 = "ff02::fb"
QTYPE_PTR = 12
def _reverse_name(ip: str) -> tuple[str, str]:
"""Return (arpa-name, address-family) for an IP."""
addr = ipaddress.ip_address(ip)
if isinstance(addr, ipaddress.IPv4Address):
return addr.reverse_pointer, "v4"
return addr.reverse_pointer, "v6"
def _parse_ptr(data: bytes) -> str | None:
"""Return first PTR target hostname in the response."""
if len(data) < 12:
return None
qd, an = struct.unpack(">HH", data[4:8])
off = 12
for _ in range(qd):
off = skip_name(data, off) + 4
for _ in range(an):
off = skip_name(data, off)
if off + 10 > len(data):
return None
rtype, _, _, rdlen = struct.unpack(">HHIH", data[off:off + 10])
off += 10
if rtype == QTYPE_PTR:
return _read_name(data, off)
off += rdlen
return None
def _read_name(data: bytes, off: int) -> str:
parts: list[str] = []
seen: set[int] = set()
while off < len(data):
b = data[off]
if b == 0:
break
if b & 0xC0 == 0xC0:
if off in seen:
break
seen.add(off)
off = ((b & 0x3F) << 8) | data[off + 1]
continue
parts.append(data[off + 1:off + 1 + b].decode(errors="replace"))
off += b + 1
return ".".join(parts)
def reverse(ip: str, timeout: float = 1.5, retries: int = 2) -> str | None:
arpa, family = _reverse_name(ip)
af = socket.AF_INET6 if family == "v6" else socket.AF_INET
dst = MDNS_ADDR6 if family == "v6" else MDNS_ADDR4
per_try = max(0.4, timeout / max(1, retries))
for _ in range(max(1, retries)):
s = socket.socket(af, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
if family == "v4":
mreq = socket.inet_aton(MDNS_ADDR4) + socket.inet_aton("0.0.0.0")
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
except OSError:
pass
s.setblocking(False)
try:
s.sendto(build_query(arpa, QTYPE_PTR), (dst, MDNS_PORT))
except OSError:
s.close()
continue
end = time.time() + per_try
while time.time() < end:
r, _, _ = select.select([s], [], [], min(0.1, max(0.0, end - time.time())))
if r:
try:
data, _ = s.recvfrom(4096)
except OSError:
continue
name = _parse_ptr(data)
if name:
s.close()
return name
s.close()
return None