mdns-termux/lib/mdns_tools/_pick.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
"""Pick the most reachable address when a host advertises several.
mDNS hosts with virtual bridges (libvirt, docker) or multiple NICs announce
every local IP. Many are not routable from us. Prefer an address in our own
subnet, then drop known-virtual ranges, else fall back to the first.
"""
import ipaddress
import socket
# Bridge/virtual ranges that are usually not reachable across hosts.
_AVOID = [
ipaddress.ip_network("192.168.122.0/24"), # libvirt default (virbr0)
ipaddress.ip_network("172.17.0.0/16"), # docker default (docker0)
ipaddress.ip_network("169.254.0.0/16"), # link-local autoconf
]
def local_ipv4() -> str | None:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
except OSError:
return None
finally:
s.close()
def _avoided(ip: str) -> bool:
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return True
return any(addr in net for net in _AVOID)
def pick_best(candidates: list[str], family: str = "v4") -> str | None:
"""Return the best-reachable address, or None if none look routable.
Returning None on all-unreachable is deliberate: it lets the caller fall
through to the next resolution method (e.g. Tailscale) instead of handing
back a dead virtual-bridge IP.
"""
uniq: list[str] = []
for c in candidates:
if c and c not in uniq:
uniq.append(c)
if not uniq:
return None
if family == "v4":
mine = local_ipv4()
if mine:
try:
my_net = ipaddress.ip_network(mine + "/24", strict=False)
same = [c for c in uniq if ipaddress.ip_address(c) in my_net]
if same:
return same[0]
except ValueError:
pass
# Drop known virtual/bridge ranges; if nothing remains, signal None.
good = [c for c in uniq if not _avoided(c)]
return good[0] if good else None
# IPv6: prefer global/ULA over link-local (fe80::)
non_ll = [c for c in uniq if not c.lower().startswith("fe80")]
return non_ll[0] if non_ll else uniq[0]