# 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 ```bash 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 ```bash mdns resolve [--family v4|v6] [--timeout 1.5] [--stale] [--no-cache] mdns reverse # 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 (foreground) mdns publish --daemon # singleton daemon, re-announce on IP change mdns publish --address # bare A record (avahi-publish -a) mdns status | stop # inspect / stop the daemon mdns sync-hosts [--dry-run] [--duration N] # write discovered hosts into hosts file mdns cache list | clear [host] ``` ### Running the publisher as a background service `mdns publish --daemon` is the robust way to keep `movil.local` reachable: - **Singleton** — a PID lock (`~/.cache/mdns/publisher.pid`) refuses a second instance. (This is what prevents the "Termux SMB" / "Termux SMB-2" duplicates you get when several publishers race for the same name.) - **Re-announce on IP change** — polls the local IP; on WiFi<->data / roaming it unregisters and re-registers with the new address, so the record never goes stale. - **Clean shutdown** — handles SIGTERM *and* SIGINT, sending mDNS goodbye packets so clients drop the entry immediately instead of waiting for the TTL. ```bash mdns publish --daemon & # or wire it into termux-services / a boot script mdns status # running (pid ...), host, ip, services, since mdns stop # graceful goodbye ``` ### `sync-hosts` — native `.local` without the wrapper Writes a managed block into the hosts file so `ping`, `curl`, `kubectl`, etc. resolve `.local` names natively (the closest you get to `nss-mdns` without root): ```bash mdns sync-hosts --dry-run # preview the block mdns sync-hosts # write it into $PREFIX/etc/hosts (Termux) or /etc/hosts ping dell-latitude3400.local # now works in any tool, no `resolve` needed ``` Only the block between the `mdns-termux managed` markers is touched; the rest of the file is preserved. Examples: ```bash 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 ```bash 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 to `tailb0bb74.ts.net`. Set to your own tailnet. - `MDNS_TIMEOUT` — seconds waiting for multicast (default `2`). - `MDNS_CACHE_PATH` — override cache location (default `~/.cache/mdns/hosts.json`). ### Shell helpers (from `config/bashrc.snippet`) ```bash alias r='resolve' pingr # ping -c 3 to resolved IP curlr [flags] # curl http:// sshr [cmd...] # ssh via resolved IP with correct HostKeyAlias whor # 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: ```bash 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: ```bash mdns publish # foreground nohup mdns publish >>~/.cache/mdns.log 2>&1 & disown # background ``` The publisher registers `.local` (`_workstation._tcp`) plus every service listed in the config. On Ctrl+C it sends proper mDNS goodbye packets. Example `services.json`: ```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 ` 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` | | Publisher daemon (singleton) | ✓ | PID lock, re-announce on IP change | | Clean goodbye on shutdown | ✓ | SIGTERM + SIGINT handled | | Hosts-file sync (nss-mdns-lite) | ✓ | `mdns sync-hosts`, managed block | | Legacy unicast queries | — | Not implemented (rarely needed) | | Known-answer suppression | — | zeroconf handles internally | ## Tests Parser/logic tests run offline (no network), via `pytest`: ```bash pip install -e ".[test]" # or: pip install pytest pytest # 20 tests: query build/parse, address pick, cache TTL, reverse, hosts block ``` ## 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 publish` running on the phone, a Linux box with `nss-mdns` resolved `movil.local` and 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 browse` on 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 - **`getent` doesn't exist on Termux.** Scripts that shell out to it silently fail. This repo uses `python -c "import socket; socket.gethostbyname(...)"` as the portable fallback on Bionic. - **Shebang matters.** `#!/data/data/com.termux/files/usr/bin/bash` is required for `declare -A`. A stray backslash (`#\!`) makes the kernel fall through to `sh`, which 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, only Tailscale resolves. - **`zeroconf` is not `pkg`-managed.** After restoring Termux from a `tar` backup, re-run `pip install zeroconf` or `mdns-publish.py` will silently die with `ModuleNotFoundError`. - **Silent failures are the enemy.** The `.bashrc` snippet deliberately logs to `~/.cache/mdns.log` instead 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 `*..ts.net`, which Bionic-libc resolves fine (it's just a regular DNS lookup). ## Related - [claude-code-termux](https://devops.ingeniumcodex.com/andresgarcia0313/claude-code-termux) — Claude Code CLI setup for Termux. Uses this repo for SSH/host resolution. ## License MIT — see `LICENSE`.