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:
<host>.localfor fast LAN access<host>.YOUR-TAILNET.ts.netfor 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 totailb0bb74.ts.net. Set to your own tailnet.MDNS_TIMEOUT— seconds for multicast (1by 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
getentdoesn't exist on Termux. Any script that shells out to it for DNS will silently fail.resolveandssh-mdns-proxyfall back topython -c "import socket; socket.gethostbyname(...)"— that's the portable way on Bionic.- Shebang matters.
#!/data/data/com.termux/files/usr/bin/bashis required fordeclare -A(associative arrays). A stray backslash (#\!) makes the kernel fall through tosh, which silently rejects bash-only syntax withSyntax 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.