v0.21.0: agent-managed split-horizon LAN resolver (internal/lanresolver)

Host-side dnsmasq the agent manages so LAN clients reach their guest directly
(same hostname + real wildcard cert, no Cloudflare hairpin). Renders local=/
+address=/ per customer (AAAA->NODATA via authoritative zone, wildcard A ->
live guest IP), forwards everything else. Manager ensures dnsmasq+base config,
discovers guest IP (pct exec ip) + domain (controller.yaml), write-if-changed +
reload. Loop (7th daemon goroutine) tracks DHCP IP changes per provisioned
guest. --selftest=lanresolver. FELHOM_DNSMASQ sudoers. Spiked live on felhom-pve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:24:58 +02:00
parent 621a09a1c5
commit a43e9813ad
7 changed files with 642 additions and 10 deletions
+38 -3
View File
@@ -28,9 +28,44 @@ type Config struct {
Hub HubConfig `json:"hub"`
Storage StorageConfig `json:"storage"`
Backup BackupConfig `json:"backup"`
Escrow EscrowConfig `json:"escrow"`
LocalAPI LocalAPIConfig `json:"local_api"`
LogLevel string `json:"log_level"` // debug|info|warn|error (default info)
Escrow EscrowConfig `json:"escrow"`
LocalAPI LocalAPIConfig `json:"local_api"`
LANResolver LANResolverConfig `json:"lan_resolver"`
LogLevel string `json:"log_level"` // debug|info|warn|error (default info)
}
// LANResolverConfig configures the host-level split-horizon DNS resolver (internal/lanresolver): a
// dnsmasq the agent manages so LAN clients reach their guest DIRECTLY at the same hostname + real cert.
// Disabled unless Enable is set. HostIP defaults to the local-API bridge IP (the host LAN anchor);
// Upstreams default to public resolvers; the loop re-checks the guest's live IP every interval.
type LANResolverConfig struct {
Enable bool `json:"enable"`
HostIP string `json:"host_ip"` // dnsmasq listen-address; default = LocalAPI bridge IP
Upstreams []string `json:"upstreams"` // forward targets for non-customer names
IntervalSeconds int `json:"interval_seconds"` // IP-freshness re-check cadence; default 300
StateDir string `json:"state_dir"` // provisioned guests under <StateDir>/guests/; default /var/lib/felhom-agent
}
// Enabled reports whether the split-horizon resolver should run.
func (l LANResolverConfig) Enabled() bool { return l.Enable }
// WithDefaults fills upstreams/interval/state-dir and derives HostIP from the local-API bind addr.
func (l LANResolverConfig) WithDefaults(localAPIListen string) LANResolverConfig {
if len(l.Upstreams) == 0 {
l.Upstreams = []string{"1.1.1.1", "8.8.8.8"}
}
if l.IntervalSeconds == 0 {
l.IntervalSeconds = 300
}
if l.StateDir == "" {
l.StateDir = "/var/lib/felhom-agent"
}
if strings.TrimSpace(l.HostIP) == "" && localAPIListen != "" {
if h, _, err := net.SplitHostPort(localAPIListen); err == nil && h != "" && h != "0.0.0.0" {
l.HostIP = h
}
}
return l
}
// LocalAPIConfig configures the per-guest local API server (doc 03 §6, slice 8A). The