* feat: bind HTTP server to multiple addresses
Support a list of bind addresses so the backend can serve on loopback and
a tailnet IP at once, without exposing every interface via 0.0.0.0.
- Config: server.host now parses a comma-separated list; a new server.hosts
YAML array takes precedence when set. Entries are trimmed, deduped, and
validated (IP or hostname); a wildcard entry collapses the set.
- Startup: one net.Listener per resolved address serving one shared
http.Server; the single-listener path is the one-element case.
- Bind-failure policy: all-fail is fatal; partial-fail warns, serves the
good addresses, and runs a cancellable background retry loop that binds
late-arriving addresses (e.g. a tailnet IP after tailscaled starts).
- Graceful shutdown stops the retry loop before closing all listeners;
covered by goleak.
- Readiness probe picks a reachable bound address, preferring loopback.
- Add ServerConfig.NonLoopbackBinds and config.IsLoopbackHost so the
sibling app-auth task can gate fail-closed auth on off-box binds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve KANDEV_SERVER_HOST over config server.hosts
server.host now wins over the server.hosts array when set, because Host
carries the KANDEV_SERVER_HOST env override and env must beat a config-file
server.hosts (env-over-file precedence and the desktop loopback contract).
The viper default for server.host is empty so an unset host is
distinguishable from an explicit one; server.hosts is used only when host is
unset. Adds regression tests covering the env-override and hosts-when-unset
paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: harden multi-bind host parsing and listener lifecycle
Address review feedback:
- Validate every host before wildcard collapse so an invalid entry is
rejected regardless of order; canonicalize IPs so equivalent forms
(::1 and 0:0:0:0:0:0:0:1) dedupe and unspecified longhand IPs count as
the wildcard.
- Probe the IPv6 loopback (::1) for an IPv6 wildcard listener instead of
127.0.0.1, so readiness doesn't stall on an IPv6-only socket.
- Make serverListeners.Stop race-free via sync.Once.
- Register t.Cleanup for the blocker listener before the fatal-assertion
path in the partial-failure test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>