mdns-termux/README.md

261 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <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 (foreground)
mdns publish --daemon # singleton daemon, re-announce on IP change
mdns publish --address <name> <ip> # 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 <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:
```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 `<hostname>.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 <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` |
| 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
`*.<your-tailnet>.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`.