feat(mdns): añadir resolver genérico + helpers de shell

- bin/resolve: hostname -> IP con orden mDNS (LAN) -> Tailscale -> caché.
  Reutilizable en cualquier comando (ping, curl, kubectl, nc).
- bin/ssh-mdns-proxy: arreglar shebang (#\! -> #!) que hacía fallar
  declare -A al caer en sh; y reemplazar getent (inexistente en Termux)
  por python socket.gethostbyname, con detección para portabilidad.
- config/bashrc.snippet: añadir aliases r/pingr/sshr/curlr. sshr usa
  HostKeyAlias para que SSH-por-IP no falle verificación de known_hosts.
- docs/03-ssh-mdns-tailscale.md: sección nueva sobre resolve + gotchas
  (getent ausente, shebang bash-only, mDNS no cruza redes).
- install.sh: incluir resolve en la lista de scripts a instalar.
- README.md: nueva fila en la tabla de componentes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrés Eduardo García Márquez 2026-07-17 09:35:59 -05:00
parent f7ad71bd3c
commit 5c4e41f6b0
6 changed files with 109 additions and 3 deletions

View File

@ -43,6 +43,7 @@ bash install.sh --dry-run
| `mdns-publish.py` | `~/.local/bin/` | Publish this device as `movil.local` (needs `zeroconf`) | | `mdns-publish.py` | `~/.local/bin/` | Publish this device as `movil.local` (needs `zeroconf`) |
| `ssh-mdns-proxy` | `~/.local/bin/` | SSH ProxyCommand: mDNS → Tailscale | | `ssh-mdns-proxy` | `~/.local/bin/` | SSH ProxyCommand: mDNS → Tailscale |
| `ssh-fallback` | `~/.local/bin/` | SSH ProxyCommand: Tailscale → LAN fallback | | `ssh-fallback` | `~/.local/bin/` | SSH ProxyCommand: Tailscale → LAN fallback |
| `resolve` | `~/.local/bin/` | Generic host resolver: mDNS → Tailscale → cache (for any command) |
| `settings.json` | `~/.claude/` | Disables auto-updater | | `settings.json` | `~/.claude/` | Disables auto-updater |
## Usage ## Usage

47
bin/resolve Executable file
View File

@ -0,0 +1,47 @@
#!/data/data/com.termux/files/usr/bin/bash
# resolve: hostname -> IP con fallback mDNS (LAN) -> Tailscale DNS -> cache
# Uso: resolve <hostname>[.local|.tailnet]
# Ej: resolve lenovo # -> 100.69.236.16
# ping $(resolve hp62a)
# curl "http://$(resolve dell):8080"
set -e
HOST="${1:-}"
[[ -z "$HOST" ]] && { echo "Uso: resolve <hostname>" >&2; exit 1; }
CACHE="$HOME/.ssh/resolve-cache"
TAILSCALE_DOMAIN="${TAILSCALE_DOMAIN:-tailb0bb74.ts.net}"
MDNS_TIMEOUT="${MDNS_TIMEOUT:-1}"
QUERY="${HOST%.local}"
QUERY="${QUERY%.${TAILSCALE_DOMAIN}}"
declare -A ALIASES=(
[dell]="dell-latitude3400"
[lenovo]="lenovo-ideapad"
)
[[ -n "${ALIASES[$QUERY]:-}" ]] && QUERY="${ALIASES[$QUERY]}"
# 1. mDNS multicast (LAN)
IP=$("$HOME/.local/bin/mdns-resolve" "${QUERY}.local" "$MDNS_TIMEOUT" 2>/dev/null || true)
# 2. Tailscale MagicDNS (remoto)
if [[ -z "$IP" ]]; then
IP=$(python -c "import socket; print(socket.gethostbyname('${QUERY}.${TAILSCALE_DOMAIN}'))" 2>/dev/null || true)
fi
# 3. Cache
if [[ -z "$IP" && -f "$CACHE" ]]; then
IP=$(grep "^${QUERY} " "$CACHE" 2>/dev/null | awk '{print $2}')
fi
if [[ -z "$IP" ]]; then
echo "Error: no se resuelve '${HOST}'" >&2
exit 1
fi
mkdir -p "$(dirname "$CACHE")"
grep -v "^${QUERY} " "$CACHE" > "${CACHE}.tmp" 2>/dev/null || true
echo "${QUERY} ${IP}" >> "${CACHE}.tmp"
mv "${CACHE}.tmp" "$CACHE"
echo "$IP"

View File

@ -24,9 +24,13 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# 1. mDNS resolution # 1. mDNS resolution
IP=$("$SCRIPT_DIR/mdns-resolve" "${QUERY}.local" 2 2>/dev/null) IP=$("$SCRIPT_DIR/mdns-resolve" "${QUERY}.local" 2 2>/dev/null)
# 2. Fallback: Tailscale DNS # 2. Fallback: Tailscale DNS (Termux carece de getent, usar python si esta disponible)
if [[ -z "$IP" ]]; then if [[ -z "$IP" ]]; then
if command -v getent >/dev/null 2>&1; then
IP=$(getent hosts "${QUERY}.${TAILSCALE_DOMAIN}" 2>/dev/null | awk '{print $1}') IP=$(getent hosts "${QUERY}.${TAILSCALE_DOMAIN}" 2>/dev/null | awk '{print $1}')
else
IP=$(python -c "import socket; print(socket.gethostbyname('${QUERY}.${TAILSCALE_DOMAIN}'))" 2>/dev/null)
fi
fi fi
# 3. Fallback: cached IP # 3. Fallback: cached IP

View File

@ -27,6 +27,21 @@ alias mdns-status='pgrep -af mdns-publish || echo "mDNS publisher is not running
alias mdns-restart='pkill -f mdns-publish; sleep 1; nohup python ~/.local/bin/mdns-publish.py >>~/.cache/mdns.log 2>&1 & disown && echo "mDNS restarted"' alias mdns-restart='pkill -f mdns-publish; sleep 1; nohup python ~/.local/bin/mdns-publish.py >>~/.cache/mdns.log 2>&1 & disown && echo "mDNS restarted"'
alias mdns-stop='pkill -f mdns-publish && echo "mDNS stopped"' alias mdns-stop='pkill -f mdns-publish && echo "mDNS stopped"'
# === Generic host resolver (resolve: mDNS -> Tailscale -> cache) ===
# Works in any command: ping $(resolve lenovo) / curl "http://$(resolve hp62a)"
alias r='resolve'
pingr() { ping -c 3 "$(resolve "$1")"; }
curlr() { local h="$1"; shift; curl "$@" "http://$(resolve "$h")"; }
# HostKeyAlias reuses the known_hosts entry for the Tailscale FQDN
# so SSH-by-IP does not fail host-key verification.
sshr() {
local h="$1"; shift
local canonical="$h"
case "$h" in dell) canonical="dell-latitude3400";; lenovo) canonical="lenovo-ideapad";; esac
ssh -o "HostKeyAlias=${canonical}.${TAILSCALE_DOMAIN:-tailb0bb74.ts.net}" \
"YOUR_USER@$(resolve "$h")" "$@"
}
# === Optional: auto-start samba + mDNS on shell login === # === Optional: auto-start samba + mDNS on shell login ===
# Uncomment the block below if you want services up automatically. # Uncomment the block below if you want services up automatically.
# Failures are logged (NOT silenced) — silent failures after a tar restore are the worst class of bug. # Failures are logged (NOT silenced) — silent failures after a tar restore are the worst class of bug.

View File

@ -10,6 +10,7 @@ Termux's Bionic libc does **not** resolve `.local` names. This repo ships small
| `mdns-publish.py` | Publish this device on the LAN as `movil.local` (or whatever you choose) using `zeroconf`. | | `mdns-publish.py` | Publish this device on the LAN as `movil.local` (or whatever you choose) using `zeroconf`. |
| `ssh-mdns-proxy` | SSH `ProxyCommand` that tries mDNS first, then Tailscale DNS. Used from `~/.ssh/config`. | | `ssh-mdns-proxy` | SSH `ProxyCommand` that tries mDNS first, then Tailscale DNS. Used from `~/.ssh/config`. |
| `ssh-fallback` | Lower-level fallback: tries Tailscale, then LAN. Useful when Tailscale is flaky. | | `ssh-fallback` | Lower-level fallback: tries Tailscale, then LAN. Useful when Tailscale is flaky. |
| `resolve` | Generic resolver for **any** command (ping, curl, kubectl…). Same order: mDNS → Tailscale → cache. |
## SSH config ## SSH config
@ -58,3 +59,41 @@ When you list hosts in `~/.ssh/config`, prefer:
2. `<host>.YOUR-TAILNET.ts.net` for off-LAN 2. `<host>.YOUR-TAILNET.ts.net` for off-LAN
Never hardcode IPs — they rotate. Never hardcode IPs — they rotate.
## Generic resolver (`resolve`) for any command
`ssh-mdns-proxy` only helps SSH. For everything else — `ping`, `curl`, `kubectl`, `nc` — use `resolve`. It applies the same lookup order and updates the cache:
```bash
resolve lenovo # -> 100.69.236.16
ping $(resolve hp62a)
curl "http://$(resolve dell):8080"
```
Environment knobs:
- `TAILSCALE_DOMAIN` — defaults to `tailb0bb74.ts.net`. Set to your own tailnet.
- `MDNS_TIMEOUT` — seconds for multicast (`1` by default, low so remote calls stay snappy).
Short aliases and helper functions to drop into `~/.bashrc` (see `config/bashrc.snippet`):
```bash
alias r='resolve'
pingr() { ping -c 3 "$(resolve "$1")"; }
sshr() {
local h="$1"; shift
local canonical="$h"
case "$h" in dell) canonical="dell-latitude3400";; lenovo) canonical="lenovo-ideapad";; esac
ssh -o "HostKeyAlias=${canonical}.${TAILSCALE_DOMAIN:-tailb0bb74.ts.net}" \
"YOUR_USER@$(resolve "$h")" "$@"
}
curlr() { local h="$1"; shift; curl "$@" "http://$(resolve "$h")"; }
```
`HostKeyAlias` in `sshr` is important: since we connect by IP, SSH would fail host-key verification otherwise. Aliasing back to the canonical Tailscale FQDN reuses the `known_hosts` entry you already trust.
## Gotchas found the hard way
- **`getent` doesn't exist on Termux.** Any script that shells out to it for DNS will silently fail. `resolve` and `ssh-mdns-proxy` fall back to `python -c "import socket; socket.gethostbyname(...)"` — that's the portable way on Bionic.
- **Shebang matters.** `#!/data/data/com.termux/files/usr/bin/bash` is required for `declare -A` (associative arrays). A stray backslash (`#\!`) makes the kernel fall through to `sh`, which silently rejects bash-only syntax with `Syntax error: "(" unexpected`.
- **mDNS off-LAN is a no-op.** Multicast (224.0.0.251) does not cross networks. Over mobile data the Tailscale fallback is the only path that works.

View File

@ -59,7 +59,7 @@ fi
# 4. Install scripts # 4. Install scripts
step 4 "Installing scripts to $BIN_DIR..." step 4 "Installing scripts to $BIN_DIR..."
run mkdir -p "$BIN_DIR" run mkdir -p "$BIN_DIR"
for script in clauded claude-check-env claude-update mdns-resolve mdns-publish.py ssh-mdns-proxy ssh-fallback; do for script in clauded claude-check-env claude-update mdns-resolve mdns-publish.py ssh-mdns-proxy ssh-fallback resolve; do
run cp "$SCRIPT_DIR/bin/$script" "$BIN_DIR/$script" run cp "$SCRIPT_DIR/bin/$script" "$BIN_DIR/$script"
run chmod +x "$BIN_DIR/$script" run chmod +x "$BIN_DIR/$script"
done done