Document the per-session DNS resolution feature
Records the design decision (one dnsmasq instance per session, not per network -- avoids the NXDOMAIN-fallthrough problem a per-network design would have hit for multi-network containers) and the three real bugs found while building it (dnsmasq's --pid-file needing daemonize mode, its default privilege drop breaking $XDG_STATE_HOME access, and REFUSED AAAA answers breaking getaddrinfo()-based tools), matching the level of detail already recorded for the other networking features in this document and in CLAUDE.md's own file-by-file reference. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -106,6 +106,108 @@ optional — when omitted, resolves to whichever single extern network the
|
||||
container joined; an error (not a silent guess) if the container joined more
|
||||
than one extern network and didn't disambiguate.
|
||||
|
||||
## DNS resolution
|
||||
|
||||
Containers on a shared `-n <network>` can resolve each other by the name
|
||||
given via `--hostname` on any network they share, plus
|
||||
`host.containers.internal` (podman's own convention) resolving to the first
|
||||
`extern` network's own gateway, if any. Implemented via `dnsmasq` — no
|
||||
resolver of any kind was referenced anywhere in this project before this,
|
||||
and `dnsmasq` isn't in `README.md`'s runtime-dependency list, so this is a
|
||||
genuinely new, but deliberately optional (best-effort, gated on
|
||||
`is_dnsmasq_available()`), dependency: a missing `dnsmasq` degrades with a
|
||||
warning rather than failing `-r/--run`.
|
||||
|
||||
**One `dnsmasq` instance per session, not per network.** The "obvious"
|
||||
design — one persistent instance per network, mirroring how the uplink
|
||||
provisions one process per network — was considered and rejected before
|
||||
being built, once a real correctness problem was worked through: a
|
||||
container joined to two networks would list two `nameserver` lines in
|
||||
`/etc/resolv.conf`, and standard stub resolvers (glibc, musl, busybox
|
||||
included) don't fall through to the next nameserver on NXDOMAIN, only on
|
||||
timeout — a name that exists only on the container's *second* network would
|
||||
silently fail to resolve, since the first nameserver's authoritative "no
|
||||
such name" ends the lookup right there. Running one instance per session
|
||||
instead sidesteps the problem entirely: it's entered into the *container's
|
||||
own* network namespace and bound to `127.0.0.1:53` there, so
|
||||
`/etc/resolv.conf` is always just `nameserver 127.0.0.1` regardless of how
|
||||
many networks were joined, and that one instance is configured (via
|
||||
dnsmasq's own repeatable `--hostsdir=<dir>`, one per joined network,
|
||||
inotify-watched so no reload signal is ever needed) to already know about
|
||||
every network the specific container asking could possibly mean — there's
|
||||
never a second nameserver to fall through to in the first place.
|
||||
|
||||
Each network gets one shared, host-global hosts-directory
|
||||
(`$XDG_STATE_HOME/slocker-lite/dns-hosts/<network>`); each session that
|
||||
joined it and was given a `--hostname` writes one record file into that
|
||||
directory (plain `<ip> <hostname>` syntax) so sibling sessions' own
|
||||
resolvers, watching the same directory, discover it — and since a session's
|
||||
own record lands in the exact directory its own resolver also watches,
|
||||
self-resolution falls out for free, no special-casing needed.
|
||||
`host.containers.internal` is generated per session instead (a small,
|
||||
private `--addn-hosts=<file>`, not shared) pointing at the first `extern`
|
||||
network's own gateway address, omitted entirely for an intern-only session.
|
||||
|
||||
### Three real bugs found while building this
|
||||
|
||||
Each confirmed by direct testing — isolating the change, reproducing the
|
||||
symptom, then confirming the fix — not assumed:
|
||||
|
||||
1. **dnsmasq only writes `--pid-file` while actually daemonizing.**
|
||||
`-d`/`--no-daemon` was tried first (a simpler model: the forked/exec'd
|
||||
pid stays the real pid throughout, matching how `nsenter`'s own in-place
|
||||
`execve()` already lets other parts of this project treat a pid as
|
||||
stable across `exec()`). It suppresses `--pid-file` entirely — confirmed
|
||||
directly: dnsmasq started and successfully read the hosts file (visible
|
||||
in its own, still-attached stdout at the time) but the pid-file this
|
||||
project polls for (the same bounded-poll shape `network_join.cpp`'s own
|
||||
`wait_for_isolated_net_namespace()` already uses) never appeared, so
|
||||
every attempt timed out. Fixed by letting dnsmasq daemonize normally
|
||||
instead: the forked/exec'd process is then only the *intermediate* one
|
||||
(reaped immediately, not tracked as the resolver's own pid), and the
|
||||
real, final daemon pid is read back from the pid-file itself once it
|
||||
appears — which also *is* the crash-orphan record `--clean-processes`
|
||||
needs, no separate write step required.
|
||||
2. **dnsmasq drops root privileges to an unprivileged user by default.**
|
||||
Broke reading anything under `$XDG_STATE_HOME` at all (typically
|
||||
`/root/.local/state/slocker-lite/...`, mode `0700` — a non-root user
|
||||
can't even traverse into `/root`), traced directly to dnsmasq's own log:
|
||||
`bad dynamic directory .../dns-hosts/<network>: Permission denied`,
|
||||
followed by every query coming back `REFUSED`. Fixed with an explicit
|
||||
`--user=root --group=root`, matching this project's already-root-only
|
||||
networking model throughout (bridges, iptables, the uplink, all already
|
||||
assume root). **Deliberately not the final answer** — tracked as a
|
||||
security follow-up in this repo's own `TODO.md`: run dnsmasq as a real
|
||||
low-privilege user instead, with the state it needs relocated somewhere
|
||||
that user can reach, rather than keeping a resolver process root for its
|
||||
entire lifetime purely to work around a directory permissions mismatch.
|
||||
3. **An AAAA query for a name with only an A record came back `REFUSED`.**
|
||||
Every record here is IPv4-only (matching `JoinedNetwork::container_ip`'s
|
||||
own existing scope) — but without `--filter-AAAA`, an AAAA query for a
|
||||
name dnsmasq otherwise knows perfectly well (an A record, just resolved
|
||||
correctly moments earlier via `nslookup`) also came back `REFUSED`, not
|
||||
a clean "no data" answer. This broke `ping <name>` outright: `ping`, like
|
||||
most `getaddrinfo()`-based tools, queries both A and AAAA together and
|
||||
treats `REFUSED` on *either* as a hard failure for the whole lookup, not
|
||||
merely "no IPv6 available" — confirmed by testing both with and without
|
||||
`--filter-AAAA` against the identical hosts record. `host.containers.internal`
|
||||
needed a second, related fix on top: serving it via dnsmasq's own
|
||||
`--address=/name/ip` option kept returning `REFUSED` for AAAA even with
|
||||
`--filter-AAAA` given — confirmed `--address` records aren't treated the
|
||||
same internally as ordinary hosts-file entries are — so it's instead
|
||||
served through the exact same mechanism as everything else (a plain,
|
||||
session-private `--addn-hosts=<file>`), which resolved it immediately.
|
||||
|
||||
**Verified end-to-end**, both on this dev machine and on the real Android
|
||||
target device (a fresh reboot, invoked consistently from `$HOME` — see this
|
||||
repo's own `TODO.md` for a separate, unresolved issue found along the way
|
||||
where invoking from a different working directory produced a completely
|
||||
separate config/state tree): two containers on a shared network resolve
|
||||
each other by name (including self-resolution) and can `ping` by name; a
|
||||
container joined to both an `intern` and an `extern` network resolves both
|
||||
its `intern` peer and `host.containers.internal` simultaneously — the
|
||||
specific scenario the per-network-instance design would have broken.
|
||||
|
||||
## Subnet / IP allocation
|
||||
|
||||
- IPv4 auto-allocates a `/24` starting at `10.168.0.0/24`, incrementing per
|
||||
|
||||
Reference in New Issue
Block a user