mdns-termux/lib/mdns_tools/cache.py

74 lines
1.8 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
"""TTL-aware cache for mDNS resolutions. JSON-backed for portability."""
import json
import os
import time
from pathlib import Path
CACHE_PATH = Path(os.environ.get(
"MDNS_CACHE_PATH",
Path.home() / ".cache" / "mdns" / "hosts.json",
))
def _load() -> dict:
if not CACHE_PATH.exists():
return {}
try:
return json.loads(CACHE_PATH.read_text())
except Exception:
return {}
def _save(data: dict) -> None:
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp = CACHE_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(data, indent=2, sort_keys=True))
tmp.replace(CACHE_PATH)
def get(host: str, family: str = "v4") -> str | None:
"""Return IP if fresh, None if missing or expired."""
entry = _load().get(f"{host}|{family}")
if not entry:
return None
if entry.get("expires", 0) < time.time():
return None
return entry.get("ip")
def get_stale(host: str, family: str = "v4") -> str | None:
"""Return IP even if expired (last-resort fallback)."""
entry = _load().get(f"{host}|{family}")
return entry.get("ip") if entry else None
def put(host: str, ip: str, family: str = "v4", ttl: int = 120) -> None:
data = _load()
ttl = max(30, min(ttl, 3600))
data[f"{host}|{family}"] = {
"ip": ip,
"ttl": ttl,
"expires": time.time() + ttl,
"updated": time.time(),
}
_save(data)
def clear(host: str | None = None) -> int:
"""Clear entries. Returns number of entries removed."""
if host is None:
n = len(_load())
if CACHE_PATH.exists():
CACHE_PATH.unlink()
return n
data = _load()
removed = [k for k in list(data) if k.split("|")[0] == host]
for k in removed:
data.pop(k)
_save(data)
return len(removed)
def all_entries() -> dict:
return _load()