mdns-termux/lib/mdns_tools/resolve.py

66 lines
2.2 KiB
Python
Raw 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
"""Hostname resolver: zeroconf when available, stdlib multicast fallback.
Public API:
resolve(host, family="v4"|"v6", timeout=1.0, retries=2, use_cache=True)
-> str | None
"""
from . import cache
from . import _stdlib_resolve as _std
from ._pick import pick_best
def _try_zeroconf(host: str, family: str, timeout: float) -> tuple[str, int] | None:
try:
from zeroconf import Zeroconf, AddressResolver, IPVersion
except ImportError:
return None
hostname = host if host.endswith(".") else host + "."
ver = IPVersion.V6Only if family == "v6" else IPVersion.V4Only
zc = Zeroconf(ip_version=IPVersion.All)
try:
r = AddressResolver(hostname)
# request() blocks up to `timeout` milliseconds
if not r.request(zc, int(timeout * 1000)):
return None
addrs = r.parsed_addresses(ver)
best = pick_best(addrs, family=family)
if not best:
return None
return best, 120
except Exception:
return None
finally:
try:
zc.close()
except Exception:
pass
def resolve(host: str, family: str = "v4", timeout: float = 1.0,
retries: int = 2, use_cache: bool = True) -> str | None:
hostname = host if host.endswith(".local") else f"{host}.local"
if use_cache:
hit = cache.get(hostname, family)
if hit:
return hit
per_try = max(0.3, timeout / max(1, retries))
# IPv4: try stdlib first (fast, no zeroconf startup cost).
if family == "v4":
for _ in range(max(1, retries)):
recs = _std.query_all(hostname, per_try)
if recs:
best = pick_best([ip for ip, _ in recs], family="v4")
ttl = next((t for ip, t in recs if ip == best), 120)
cache.put(hostname, best, family=family, ttl=ttl or 120)
return best
# IPv6 or IPv4-stdlib-miss: try zeroconf (heavier but RFC-complete).
for _ in range(max(1, retries)):
res = _try_zeroconf(hostname, family, per_try)
if res:
ip, ttl = res
cache.put(hostname, ip, family=family, ttl=ttl)
return ip
return None