mdns-termux/lib/mdns_tools/browse.py

60 lines
2.0 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
"""Service discovery on the local network (DNS-SD).
browse(service_type=None, duration=3.0) -> list of dicts
"""
import time
from zeroconf import Zeroconf, ServiceBrowser, ServiceListener, ZeroconfServiceTypes
class _Collector(ServiceListener):
def __init__(self) -> None:
self.items: dict[str, dict] = {}
def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802
info = zc.get_service_info(type_, name, timeout=1500)
if info is None:
return
addrs = [a for a in (info.parsed_addresses() or [])]
self.items[name] = {
"name": name,
"type": type_,
"server": (info.server or "").rstrip("."),
"port": info.port,
"addresses": addrs,
"txt": {
k.decode(errors="replace"):
(v.decode(errors="replace") if isinstance(v, (bytes, bytearray)) else v)
for k, v in (info.properties or {}).items() if k
},
}
def update_service(self, *a, **k) -> None: # noqa: N802
return
def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: # noqa: N802
self.items.pop(name, None)
def list_types(duration: float = 3.0) -> list[str]:
"""Return all service types advertised on the LAN."""
return sorted(ZeroconfServiceTypes.find(timeout=duration))
def browse(service_type: str | None = None, duration: float = 3.0) -> list[dict]:
"""Discover services. If service_type is None, discover all types first."""
zc = Zeroconf()
try:
types = [service_type] if service_type else list_types(duration=duration / 2)
if not types:
return []
listener = _Collector()
browsers = [ServiceBrowser(zc, t, listener) for t in types]
time.sleep(duration if not service_type else duration)
for b in browsers:
b.cancel()
return sorted(listener.items.values(),
key=lambda x: (x.get("type", ""), x.get("name", "")))
finally:
zc.close()