mdns-termux/lib/mdns_tools/publish.py

98 lines
2.9 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
"""Publish this device on the LAN: hostname + optional services.
Config example (config/services.example.yaml or JSON):
hostname: movil
services:
- {type: _ssh._tcp, port: 8022, name: "Termux SSH"}
- {type: _smb._tcp, port: 4450, name: "Termux SMB"}
"""
import socket
import time
from pathlib import Path
from zeroconf import Zeroconf, ServiceInfo
def _local_ipv4() -> str:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
except OSError:
return "127.0.0.1"
finally:
s.close()
def _load_config(path: str | None) -> dict:
if not path:
return {"hostname": "movil", "services": []}
p = Path(path).expanduser()
if not p.exists():
return {"hostname": "movil", "services": []}
text = p.read_text()
if p.suffix in (".yml", ".yaml"):
try:
import yaml # type: ignore
return yaml.safe_load(text) or {}
except ImportError:
print("warning: PyYAML not installed, using minimal parser")
import json
try:
return json.loads(text)
except Exception:
return {"hostname": "movil", "services": []}
def run(config_path: str | None = None,
hostname: str | None = None,
verbose: bool = True) -> None:
"""Publish hostname + services and block forever."""
cfg = _load_config(config_path)
host = hostname or cfg.get("hostname", "movil")
ip = _local_ipv4()
zc = Zeroconf()
infos: list[ServiceInfo] = []
# Register hostname via _workstation._tcp so the device shows up in browsers
ws = ServiceInfo(
type_="_workstation._tcp.local.",
name=f"{host} [{ip.replace('.', '-')}]._workstation._tcp.local.",
addresses=[socket.inet_aton(ip)],
port=9,
server=f"{host}.local.",
)
zc.register_service(ws)
infos.append(ws)
for svc in (cfg.get("services") or []):
try:
t = svc["type"].rstrip(".") + ".local."
n = f'{svc.get("name", host)}.{t}'
info = ServiceInfo(
type_=t,
name=n,
addresses=[socket.inet_aton(ip)],
port=int(svc["port"]),
server=f"{host}.local.",
properties=svc.get("txt", {}),
)
zc.register_service(info)
infos.append(info)
except Exception as e:
print(f"warning: could not register {svc}: {e}")
if verbose:
print(f"[mdns] published {host}.local ({ip}), {len(infos)} record(s). Ctrl+C to stop.")
try:
while True:
time.sleep(60)
except KeyboardInterrupt:
pass
finally:
for i in infos:
try:
zc.unregister_service(i)
except Exception:
pass
zc.close()
if verbose:
print("[mdns] stopped, goodbye packets sent.")