claude-code-termux/docs/03-ssh-mdns-tailscale.md

4.4 KiB

03 — SSH, mDNS and Tailscale on Termux

Termux's Bionic libc does not resolve .local names. This repo ships small Python helpers that fix this without needing root or a custom resolver.

Scripts involved

Script Role
mdns-resolve Resolve <host>.local via raw multicast UDP. Returns the IP or non-zero.
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-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

Drop the contents of config/ssh_config.example into ~/.ssh/config (or merge it).

The key entries:

Host *.local
    ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p

Host lenovo-ideapad
    ProxyCommand ~/.local/bin/ssh-mdns-proxy %h %p

Host *.YOUR-TAILNET.ts.net
    User YOUR_USER

Adjust the username and the tailnet domain. The proxy command will try mDNS first, then fall through to your Tailscale magic-DNS name (env TAILSCALE_DOMAIN).

Publishing this device as movil.local

If you want the device to be reachable as movil.local from your LAN:

# One-shot foreground (to see errors):
python ~/.local/bin/mdns-publish.py

# Background (typical use, from ~/.bashrc):
nohup python ~/.local/bin/mdns-publish.py >>~/.cache/mdns.log 2>&1 &
disown

Gotcha: zeroconf (the pip package) is not part of a pkg-managed install. If you restore Termux from a tar backup, zeroconf will be missing from $PREFIX/lib/python*/site-packages and the script will silently fail with ModuleNotFoundError. Re-run pip install zeroconf after every restore.

Logging, not silencing

The .bashrc auto-start lines in this repo (config/bashrc.snippet) deliberately log to a file instead of /dev/null. Silent failures are the single most painful class of bug after a restore — keep the logs.

Tailscale

Tailscale on Termux runs as the Android app, not as a CLI inside Termux. You can't tailscale up from a shell. The app provides magic DNS for *.YOUR-TAILNET.ts.net, which Bionic-libc resolves fine (it's just a regular DNS query).

When you list hosts in ~/.ssh/config, prefer:

  1. <host>.local for fast LAN access
  2. <host>.YOUR-TAILNET.ts.net for off-LAN

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:

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):

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.