Add mDNS / .local discovery to live USB pre-build plan (Claude)

Third access path for the live USB, sitting below Tailscale MagicDNS but
above the bare DHCP-IP fallback:

    ssh clawdie@clawdie-live            (Tailscale MagicDNS — preferred)
    ssh clawdie@clawdie-live.local      (mDNS / Avahi — always-on LAN)
    ssh clawdie@<dhcp-ip>               (last-resort fallback)

The implementation cost is small because Avahi is already in the live
dependency closure. The packaged ssh.service file advertises _ssh._tcp
on port 22, so no Avahi config edits are needed — we just enable the
daemon, add nss_mdns for outbound .local resolution, and patch
nsswitch.conf.

Doc changes (no code yet, implementation lands on the next build):

- BUILD.md gets a "LAN discovery (mDNS / Avahi)" subsection alongside
  the SSH key note, with the three-path priority order, the
  implementation contract, the scope-discipline reminder that
  Clawdie internal services stay on home.arpa (not .local), and the
  multicast-hostile-network caveat.

- doc/LIVE-SESSION-REVIEW.md gets a parallel "Pre-build plan: mDNS /
  .local discovery for the live USB" section with the requirements
  table (matching the format used for the SSH plan), the concrete
  package/rc.conf/nsswitch contract, and Level-1 static-check hooks
  for TESTING.md to consume once code lands.

Scope discipline carried explicitly in both docs:

  home.arpa = Clawdie internal DNS (ai/cms/git/<tenant>)
  .local    = mDNS LAN discovery for the live USB only

RFC 6762 reserves .local for mDNS; mixing it with internal service
names breaks both. The next-build implementation must not move internal
services to .local.

Package choice fixed:

  avahi-app   (already transitive; list explicitly to pin contract)
  nss_mdns    (small, not currently in closure)

Not mDNSResponder — less stack churn and avahi already ships the
ssh.service advertisement we want.

Build: pass — sh -n build.sh
Tests: pass — git diff --check
This commit is contained in:
Sam & Claude 2026-05-18 12:51:39 +02:00
parent 2b13e11bc9
commit c66d060867
2 changed files with 130 additions and 0 deletions

View file

@ -186,6 +186,49 @@ themselves after first boot or build their own image. The dedicated
See `doc/LIVE-SESSION-REVIEW.md` for the full pre-build plan and TESTING
hooks.
### LAN discovery (mDNS / Avahi)
The live USB advertises itself on the local network as
`clawdie-live.local` so the operator can SSH/`scp` without finding the
DHCP-assigned IP each boot. Two access paths in priority order:
```sh
# Preferred when Tailscale was passed at build time:
ssh clawdie@clawdie-live # MagicDNS
# Always-on LAN discovery — no Tailscale required:
ssh clawdie@clawdie-live.local # mDNS / Avahi
# Last-resort fallback if multicast is blocked on the network:
ssh clawdie@<dhcp-ip-from-router>
```
The implementation contract:
- Explicit packages in `packages/pkg-list-live-operator.txt`:
`avahi-app` (already transitive in the closure, list explicitly to
pin the contract) and `nss_mdns`.
- `avahi_daemon_enable="YES"` in live rc.conf. `dbus_enable="YES"` is
already set; Avahi depends on it.
- `/etc/nsswitch.conf` `hosts:` line set to
`files mdns_minimal [NOTFOUND=return] dns mdns` so `.local` names
resolve from the live USB itself.
- No config changes to the packaged Avahi `ssh.service` — its
`_ssh._tcp` advertisement is what we want.
**Scope discipline:** mDNS is **only** for LAN discovery of the live
USB itself. Clawdie's internal service names continue to live under
`home.arpa` (`ai.home.arpa`, `cms.home.arpa`, `git.home.arpa`,
`<tenant>.home.arpa`). Do not switch internal services to `.local`
RFC 6762 reserves `.local` for mDNS, and mixing the two namespaces
breaks both.
**Network caveat:** some corporate networks, hotel Wi-Fi, and isolated
guest VLANs block multicast traffic. In those environments
`clawdie-live.local` won't resolve and the operator falls back to the
Tailscale path (if configured) or the DHCP IP path. Worth knowing
before debugging "mDNS doesn't work" on a hostile network.
---
## Build Output and Provenance

View file

@ -521,6 +521,93 @@ their hardware. For **dev/test images** that's the right tradeoff. For
their own image with their own key. The dedicated `clawdie-live-usb` key
suggestion above keeps personal keys off shareable images.
### Pre-build plan: mDNS / `.local` discovery for the live USB
The SSH plan above gives the operator three access paths (Tailscale,
LAN-via-DHCP-IP, and TTY). The DHCP-IP path requires knowing the IP. mDNS
fixes that for any LAN that allows multicast: the live USB advertises itself
as `clawdie-live.local` and the operator's laptop resolves the name without
DHCP-server inspection.
#### Why mDNS — and what it is *not*
- **mDNS is LAN discovery for the live USB itself.** It is **not** Clawdie's
internal DNS namespace. Internal service names stay under `home.arpa`:
`ai.home.arpa`, `cms.home.arpa`, `git.home.arpa`, `<tenant>.home.arpa`.
- RFC 6762 reserves the `.local` TLD for mDNS. Mixing internal services
into `.local` breaks both namespaces — don't do it.
- mDNS sits **below** Tailscale's MagicDNS in the priority order. The
tailnet path is preferred when configured because it works across NAT
and uses unicast; mDNS is the always-on fallback when Tailscale is
absent or down.
#### Implementation requirements before build
| Requirement | Implementation |
|---|---|
| Advertise live USB on LAN | `avahi_daemon_enable="YES"` in live rc.conf |
| Stable mDNS name | hostname is already `clawdie-live`, so the advertised name becomes `clawdie-live.local` automatically |
| Advertise SSH service | use the packaged Avahi `ssh.service` (`_ssh._tcp` on port 22) — no config changes needed |
| Resolve `.local` names *from* the USB | add `nss_mdns` and patch `/etc/nsswitch.conf` `hosts:` line |
| Keep Clawdie internal names sane | continue using `home.arpa`; do not move internal services to `.local` |
#### Concrete contract
- **Packages.** Add explicitly to `packages/pkg-list-live-operator.txt`:
- `avahi-app` — already transitive in the live closure, but list
explicitly so the contract is pinned (matches the pattern used for
`wifi-firmware-kmod`).
- `nss_mdns` — not currently included; small footprint; required for
`.local` resolution from the USB.
- **rc.conf.** Add `avahi_daemon_enable="YES"`. `dbus_enable="YES"` is
already set in `build.sh:759`; Avahi depends on it.
- **nsswitch.conf.** Replace the `hosts:` line with:
```text
hosts: files mdns_minimal [NOTFOUND=return] dns mdns
```
`mdns_minimal` short-circuits non-`.local` lookups back to `dns` so we
don't multicast every hostname on the LAN. Trailing `mdns` handles
`.local` names not caught by `mdns_minimal`.
- **Avahi service file.** No edits. The packaged
`/usr/local/etc/avahi/services/ssh.service` advertises `_ssh._tcp` on
port 22, which is what we want once `sshd_enable="YES"` is in place
(see SSH plan above).
- **No package choice churn.** Use `avahi-app` + `nss_mdns`, not
`mDNSResponder`. The Avahi stack is already in the live closure and
ships the SSH service file we need.
#### Caveat: multicast-hostile networks
Some corporate networks, hotel Wi-Fi, and guest VLANs filter multicast
traffic. On those networks `clawdie-live.local` will fail to resolve and
the operator falls back to either the Tailscale path (if configured) or
the DHCP-IP path. Worth knowing before assuming "mDNS doesn't work" is a
build bug.
#### Testing hooks (live USB, after boot)
```sh
service avahi_daemon status
avahi-browse -at # should show _ssh._tcp on clawdie-live
avahi-resolve-host-name clawdie-live.local # should return an IP
# From another machine on the same LAN:
ssh clawdie@clawdie-live.local
scp clawdie@clawdie-live.local:/var/tmp/clawdie-xsession-errors-last.log .
```
Static checks for TESTING.md Level 1 (after image mount):
```sh
grep -E '^avahi_daemon_enable="YES"' /mnt/etc/rc.conf
grep -E '^hosts:.*mdns_minimal' /mnt/etc/nsswitch.conf
pkg -r /mnt info -e avahi-app
pkg -r /mnt info -e nss_mdns
test -f /mnt/usr/local/etc/avahi/services/ssh.service
```
## Audio stack — see BUILD.md
The PulseAudio creep investigation moved to `BUILD.md` once the