mdns-termux/bin/mdns

125 lines
4.3 KiB
Plaintext
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
#!/data/data/com.termux/files/usr/bin/python3
"""mdns: unified CLI for mDNS resolve / browse / publish / reverse / cache.
Loads lib/mdns_tools/ from the repo root. Usable both from an install into
~/.local/bin (installer sets a symlink pointing at the repo checkout) and
directly from a clone.
"""
import argparse
import json
import os
import sys
from pathlib import Path
_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_ROOT / "lib"))
from mdns_tools import __version__ # noqa: E402
def _cmd_resolve(a: argparse.Namespace) -> int:
from mdns_tools import resolve as _r, cache as _c
ip = _r.resolve(a.host, family=a.family, timeout=a.timeout,
retries=a.retries, use_cache=not a.no_cache)
if ip:
print(ip)
return 0
if a.stale:
ip = _c.get_stale(a.host if a.host.endswith(".local") else f"{a.host}.local",
family=a.family)
if ip:
print(ip)
return 0
print(f"error: could not resolve '{a.host}'", file=sys.stderr)
return 1
def _cmd_reverse(a: argparse.Namespace) -> int:
from mdns_tools import reverse as _rev
name = _rev.reverse(a.ip, timeout=a.timeout, retries=a.retries)
if name:
print(name)
return 0
print(f"error: no PTR for {a.ip}", file=sys.stderr)
return 1
def _cmd_browse(a: argparse.Namespace) -> int:
from mdns_tools import browse as _br # loads zeroconf
if a.list_types:
for t in _br.list_types(duration=a.duration):
print(t)
return 0
items = _br.browse(service_type=a.type, duration=a.duration)
if a.json:
print(json.dumps(items, indent=2))
return 0
for it in items:
addrs = ", ".join(it.get("addresses") or []) or "-"
print(f'{it["type"]:<30} {it.get("server","-"):<28} {addrs}:{it.get("port","-")}')
return 0
def _cmd_publish(a: argparse.Namespace) -> int:
from mdns_tools import publish as _p # loads zeroconf
_p.run(config_path=a.config, hostname=a.hostname, verbose=not a.quiet)
return 0
def _cmd_cache(a: argparse.Namespace) -> int:
from mdns_tools import cache as _c
if a.op == "list":
for k, v in _c.all_entries().items():
host, fam = k.split("|") if "|" in k else (k, "v4")
print(f'{host:<40} {fam} {v.get("ip"):<40} ttl={v.get("ttl")}')
elif a.op == "clear":
n = _c.clear(a.host)
print(f"removed {n} entries")
return 0
def main() -> int:
p = argparse.ArgumentParser(prog="mdns", description="mDNS / DNS-SD tools")
p.add_argument("--version", action="version", version=f"mdns {__version__}")
sub = p.add_subparsers(dest="cmd", required=True)
r = sub.add_parser("resolve", help="hostname -> IP")
r.add_argument("host")
r.add_argument("--family", choices=["v4", "v6"], default="v4")
r.add_argument("--timeout", type=float, default=1.5)
r.add_argument("--retries", type=int, default=2)
r.add_argument("--no-cache", action="store_true")
r.add_argument("--stale", action="store_true", help="accept expired cache as last resort")
r.set_defaults(func=_cmd_resolve)
rv = sub.add_parser("reverse", help="IP -> hostname (PTR)")
rv.add_argument("ip")
rv.add_argument("--timeout", type=float, default=1.5)
rv.add_argument("--retries", type=int, default=2)
rv.set_defaults(func=_cmd_reverse)
b = sub.add_parser("browse", help="discover services on the LAN")
b.add_argument("--type", help="e.g. _ssh._tcp.local. (default: all types)")
b.add_argument("--duration", type=float, default=3.0)
b.add_argument("--json", action="store_true")
b.add_argument("--list-types", action="store_true")
b.set_defaults(func=_cmd_browse)
pu = sub.add_parser("publish", help="advertise this device + services")
pu.add_argument("--config", help="path to services YAML/JSON")
pu.add_argument("--hostname")
pu.add_argument("--quiet", action="store_true")
pu.set_defaults(func=_cmd_publish)
ca = sub.add_parser("cache", help="inspect/clear the resolver cache")
ca.add_argument("op", choices=["list", "clear"])
ca.add_argument("host", nargs="?")
ca.set_defaults(func=_cmd_cache)
args = p.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())