a43e9813ad
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>
81 lines
2.7 KiB
Go
81 lines
2.7 KiB
Go
package lanresolver
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRenderGuestDropin(t *testing.T) {
|
|
d := RenderGuestDropin("demo-felhom", "demo-felhom.eu", "192.168.0.151")
|
|
// The load-bearing pair: local= makes dnsmasq authoritative (AAAA→NODATA), address= is the wildcard A.
|
|
if !strings.Contains(d, "local=/demo-felhom.eu/\n") {
|
|
t.Errorf("missing authoritative local= line (AAAA suppression):\n%s", d)
|
|
}
|
|
if !strings.Contains(d, "address=/demo-felhom.eu/192.168.0.151\n") {
|
|
t.Errorf("missing wildcard address= line:\n%s", d)
|
|
}
|
|
}
|
|
|
|
func TestRenderBase(t *testing.T) {
|
|
b := RenderBase("192.168.0.162", []string{"1.1.1.1", "8.8.8.8"})
|
|
for _, want := range []string{
|
|
"bind-interfaces\n", "listen-address=192.168.0.162\n", "listen-address=127.0.0.1\n",
|
|
"no-resolv\n", "server=1.1.1.1\n", "server=8.8.8.8\n",
|
|
} {
|
|
if !strings.Contains(b, want) {
|
|
t.Errorf("base config missing %q:\n%s", want, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseInet(t *testing.T) {
|
|
// Exact shape of `pct exec <vmid> -- ip -4 -o addr show dev eth0` (verified live).
|
|
live := `2: eth0 inet 192.168.0.151/24 brd 192.168.0.255 scope global dynamic eth0\ valid_lft 4765sec preferred_lft 4765sec`
|
|
if got := parseInet(live); got != "192.168.0.151" {
|
|
t.Errorf("parseInet = %q, want 192.168.0.151", got)
|
|
}
|
|
// Pre-lease window: no inet line → empty (must NOT be treated as a real IP).
|
|
if got := parseInet(`2: eth0 inet6 fe80::be24:11ff:fec7:409/64 scope link`); got != "" {
|
|
t.Errorf("parseInet(no v4) = %q, want empty", got)
|
|
}
|
|
if got := parseInet(""); got != "" {
|
|
t.Errorf("parseInet(empty) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestParseDomain(t *testing.T) {
|
|
yaml := `customer:
|
|
domain: demo-felhom.eu
|
|
id: demo-felhom
|
|
email: admin@felhom.eu
|
|
`
|
|
if got := parseDomain(yaml); got != "demo-felhom.eu" {
|
|
t.Errorf("parseDomain = %q, want demo-felhom.eu", got)
|
|
}
|
|
if got := parseDomain(` domain: "quoted-domain.eu"`); got != "quoted-domain.eu" {
|
|
t.Errorf("parseDomain(quoted) = %q, want quoted-domain.eu", got)
|
|
}
|
|
if got := parseDomain("no domain here\n"); got != "" {
|
|
t.Errorf("parseDomain(absent) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestDropinNameSanitize(t *testing.T) {
|
|
if got := DropinName("demo-felhom"); got != "felhom-demo-felhom.conf" {
|
|
t.Errorf("DropinName = %q", got)
|
|
}
|
|
// Hostile/odd id collapses to a safe filename.
|
|
if got := DropinName("../evil id"); got != "felhom-evil-id.conf" {
|
|
t.Errorf("DropinName(hostile) = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDeriveHostIP(t *testing.T) {
|
|
if got := DeriveHostIP("192.168.0.162:8443"); got != "192.168.0.162" {
|
|
t.Errorf("DeriveHostIP = %q, want 192.168.0.162", got)
|
|
}
|
|
if got := DeriveHostIP("0.0.0.0:8443"); got != "" {
|
|
t.Errorf("DeriveHostIP(0.0.0.0) = %q, want empty (require explicit)", got)
|
|
}
|
|
}
|