7.4 KiB
mdns-termux
mDNS + Tailscale hostname resolution for Termux (Android).
Termux runs on Android's Bionic libc, which does not resolve .local names
and lacks getent. This repo ships small, dependency-light helpers that fix
that — without root, and without a custom system resolver.
Works on any network: LAN via multicast, off-LAN via Tailscale MagicDNS.
What's in here
Unified CLI: mdns with five subcommands (resolve, browse, publish,
reverse, cache). Backed by zeroconf for RFC-complete behavior — TTL
cache, IPv4 + IPv6, service discovery, probing on publish, goodbye packets
on exit.
| Script | Role |
|---|---|
mdns |
Unified CLI (zeroconf-backed). Preferred entry point. |
mdns-resolve |
Standalone stdlib fallback: works with zero dependencies. |
mdns-publish.py |
Compat shim over mdns publish for existing aliases. |
ssh-mdns-proxy |
SSH ProxyCommand: mDNS → Tailscale DNS → cached IP. |
ssh-fallback |
SSH ProxyCommand: Tailscale → LAN, for flaky Tailscale. |
resolve |
Bash wrapper: mdns → stdlib → Tailscale → cache, usable in any command. |
Python library under lib/mdns_tools/ (each module < 100 lines):
resolve.py, browse.py, publish.py, reverse.py, cache.py,
_stdlib_resolve.py (dep-free A-record resolver).
Install
git clone https://devops.ingeniumcodex.com/andresgarcia0313/mdns-termux.git
cd mdns-termux
bash install.sh # or --dry-run first
Then follow the printed reminders (append bashrc.snippet, set your
TAILSCALE_DOMAIN, optionally merge ssh_config.example).
Usage
mdns CLI
mdns resolve <host> [--family v4|v6] [--timeout 1.5] [--stale] [--no-cache]
mdns reverse <ip> # PTR: IP -> hostname
mdns browse [--type _svc._tcp.local.] [--json] [--list-types]
mdns publish [--config ~/.config/mdns/services.json] [--hostname NAME]
mdns cache list | clear [host]
Examples:
mdns browse --list-types # every service type on the LAN
mdns browse --type _ssh._tcp.local. # every SSH host on the LAN
mdns resolve lenovo-ideapad # -> 192.168.1.106 (LAN, fast stdlib path)
mdns resolve lenovo-ideapad --family v6 # -> fe80::... (via zeroconf)
mdns reverse 192.168.1.17 # -> dell-latitude3400.local
mdns publish # advertise services from ~/.config/mdns/services.json
mdns cache list # inspect TTL-aware cache
resolve (bash) — for any command
resolve lenovo # -> 100.69.236.16
ping $(resolve hp62a)
curl "http://$(resolve dell):8080"
kubectl --server="https://$(resolve master):6443" get nodes
Environment knobs:
TAILSCALE_DOMAIN— defaults totailb0bb74.ts.net. Set to your own tailnet.MDNS_TIMEOUT— seconds waiting for multicast (default2).MDNS_CACHE_PATH— override cache location (default~/.cache/mdns/hosts.json).
Shell helpers (from config/bashrc.snippet)
alias r='resolve'
pingr <host> # ping -c 3 to resolved IP
curlr <host> [flags] # curl http://<resolved-ip>
sshr <host> [cmd...] # ssh via resolved IP with correct HostKeyAlias
whor <ip> # PTR reverse: IP -> hostname
mdns-types # list all service types advertised on LAN
mdns-ssh / mdns-smb / mdns-http # browse specific service types
mdns-cache / mdns-forget # inspect / clear the resolver cache
SSH by hostname (via ProxyCommand)
Drop config/ssh_config.example into ~/.ssh/config. Then:
ssh lenovo # resolves via mDNS then Tailscale, connects
ssh lenovo.local # forces mDNS-first path
Publishing this device on the LAN
Edit ~/.config/mdns/services.json (the installer seeds a template), then:
mdns publish # foreground
nohup mdns publish >>~/.cache/mdns.log 2>&1 & disown # background
The publisher registers <hostname>.local (_workstation._tcp) plus every
service listed in the config. On Ctrl+C it sends proper mDNS goodbye packets.
Example services.json:
{
"hostname": "movil",
"services": [
{"type": "_ssh._tcp", "port": 8022, "name": "Termux SSH"},
{"type": "_smb._tcp", "port": 4450, "name": "Termux SMB"}
]
}
Behavior by network
| Scenario | Path | Typical latency |
|---|---|---|
| On home LAN | stdlib multicast → zeroconf (IPv6 or miss) | < 1 s (IPv4) |
| Mobile data / away | multicast times out → Tailscale MagicDNS | < 2 s |
| No Tailscale | all fail → TTL-aware cache → legacy flat cache | instant |
Cache honors real mDNS TTL (default 30 – 3600 s) and stores IPv4 + IPv6
separately. mdns resolve --stale <host> accepts expired entries as a
last-resort fallback.
RFC coverage (what's implemented)
| Feature | Status | Notes |
|---|---|---|
| A / AAAA hostname resolution | ✓ | stdlib for A, zeroconf for AAAA |
| PTR reverse lookup | ✓ | stdlib multicast, IPv4 + IPv6 |
| DNS-SD service browsing | ✓ | mdns browse, all types or one |
| Publisher (hostname + services) | ✓ | via zeroconf, with probing + goodbye |
| Multicast group membership | ✓ | IP_ADD_MEMBERSHIP on all sockets |
| Retries with backoff | ✓ | 2 attempts, split timeout |
| TTL-honoring cache | ✓ | JSON at ~/.cache/mdns/hosts.json |
| Legacy unicast queries | — | Not implemented (rarely needed) |
| Known-answer suppression | — | zeroconf handles internally |
Gotchas found the hard way
getentdoesn't exist on Termux. Scripts that shell out to it silently fail. This repo usespython -c "import socket; socket.gethostbyname(...)"as the portable fallback on Bionic.- Shebang matters.
#!/data/data/com.termux/files/usr/bin/bashis required fordeclare -A. A stray backslash (#\!) makes the kernel fall through tosh, which 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, only Tailscale resolves.
zeroconfis notpkg-managed. After restoring Termux from atarbackup, re-runpip install zeroconformdns-publish.pywill silently die withModuleNotFoundError.- Silent failures are the enemy. The
.bashrcsnippet deliberately logs to~/.cache/mdns.loginstead of/dev/null. Keep it that way.
Tailscale on Termux
Tailscale runs as the Android app, not as a Termux CLI — tailscale up
inside a shell won't work. The app provides MagicDNS for
*.<your-tailnet>.ts.net, which Bionic-libc resolves fine (it's just a
regular DNS lookup).
Related
- claude-code-termux — Claude Code CLI setup for Termux. Uses this repo for SSH/host resolution.
License
MIT — see LICENSE.