Añade las dos funciones que Avahi/Bonjour tenían y faltaban: - `mdns browse --watch`: monitor DNS-SD en vivo (eventos +/=/-), equivalente a `avahi-browse -a`. Sin --type descubre tipos dinámicamente via el meta-servicio _services._dns-sd._udp.local. y adjunta un browser por tipo. Nuevo modulo lib/mdns_tools/watch.py. - `mdns publish --address <nombre> <ip>`: publica un A record puro hostname->IP, equivalente a `avahi-publish -a`. Robustez del publisher: - Maneja NonUniqueNameException (probing RFC 6762): si el nombre ya esta tomado, deja que zeroconf renombre en vez de crashear. - `mdns publish` sin --config auto-carga ~/.config/mdns/services.json. Interoperabilidad validada en LAN real (documentada en README): - Termux -> Avahi: con `mdns publish` activo, un Linux con nss-mdns resolvio `movil.local` y le hizo ping sin configuracion extra (getent + ping OK). - Avahi -> Termux: `mdns browse` ve los servicios que publican equipos Avahi/Bonjour (SMB, SSH, _workstation, Chromecast, AirPlay...). Al usar zeroconf (RFC 6762/6763), Mac/Linux/Windows descubren y resuelven lo publicado y viceversa, sin cliente especial. README: seccion de interoperabilidad, tabla RFC ampliada, ejemplos --watch y --address. bashrc.snippet: alias mdns-watch. publish.py recortado a <100. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| bin | ||
| config | ||
| lib/mdns_tools | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
| install.sh | ||
README.md
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 browse --watch [--type ...] [--no-resolve] # live monitor (avahi-browse -a)
mdns publish [--config PATH] [--hostname NAME] # services from JSON
mdns publish --address <name> <ip> # bare A record (avahi-publish -a)
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 publish --address printer 192.168.1.50 # publish printer.local -> that IP
mdns browse --watch # live +/=/- events, all types
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 |
| Live service monitor | ✓ | mdns browse --watch (like avahi-browse) |
| Publisher (hostname + services) | ✓ | via zeroconf, with probing + goodbye |
| Bare hostname->IP publish | ✓ | mdns publish --address (avahi-publish -a) |
| Name-conflict handling | ✓ | RFC 6762 probe + auto-rename |
| 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 |
Interoperability (Avahi / Bonjour)
Because publishing goes through zeroconf (a full RFC 6762 / 6763
implementation), anything you advertise is visible to standard mDNS stacks —
no special client needed on the other end.
Verified on a real LAN:
- Termux → Avahi (Linux): with
mdns publishrunning on the phone, a Linux box withnss-mdnsresolvedmovil.localand pinged it, with no extra config:$ getent hosts movil.local 192.168.1.112 movil.local $ ping movil.local # 64 bytes ... time=3.84 ms - Avahi → Termux:
mdns browseon the phone sees services published by Avahi/Bonjour hosts (SMB, SSH,_workstation, Chromecast, AirPlay, …).
So a Mac (Bonjour), a Linux box (Avahi), or a Windows machine (Bonjour Print Services) will all discover and resolve what this tool publishes, and vice versa.
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.