Support custom domains. Fix host-record/address resolution.

This commit is contained in:
2026-07-13 00:26:13 +00:00
parent 65c7e56b0a
commit 1e7d93256e
12 changed files with 336 additions and 132 deletions

View File

@@ -12,7 +12,11 @@ func entry(domain, ip string) DNSEntry {
return DNSEntry{Domain: domain, IP: ip}
}
// Test: Generate with entries produces expected DNS entries
func wildcardEntry(domain, ip string) DNSEntry {
return DNSEntry{Domain: domain, IP: ip, Wildcard: true}
}
// Test: Generate with exact-match entries produces host-record= directives
func TestGenerate_WithEntries(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
entries := []DNSEntry{
@@ -22,17 +26,11 @@ func TestGenerate_WithEntries(t *testing.T) {
out := g.Generate(nil, entries)
if !strings.Contains(out, "local=/cloud.example.com/") {
t.Errorf("expected local=/ for domain, got:\n%s", out)
if !strings.Contains(out, "host-record=cloud.example.com,192.168.1.10") {
t.Errorf("expected host-record for domain, got:\n%s", out)
}
if !strings.Contains(out, "address=/cloud.example.com/192.168.1.10") {
t.Errorf("expected address entry for domain, got:\n%s", out)
}
if !strings.Contains(out, "local=/internal.example.com/") {
t.Errorf("expected local=/ for internal domain, got:\n%s", out)
}
if !strings.Contains(out, "address=/internal.example.com/192.168.1.10") {
t.Errorf("expected address entry for internal domain, got:\n%s", out)
if !strings.Contains(out, "host-record=internal.example.com,192.168.1.10") {
t.Errorf("expected host-record for internal domain, got:\n%s", out)
}
if !strings.Contains(out, "server=1.1.1.1") {
t.Errorf("expected upstream DNS server, got:\n%s", out)
@@ -62,8 +60,8 @@ func TestGenerate_EntryWithoutIP(t *testing.T) {
out := g.Generate(nil, []DNSEntry{entry("cloud.example.com", "")})
if strings.Contains(out, "address=/") {
t.Errorf("expected no address= entries for empty IP, got:\n%s", out)
if strings.Contains(out, "host-record=") {
t.Errorf("expected no host-record entries for empty IP, got:\n%s", out)
}
}
@@ -198,78 +196,98 @@ func TestNewConfigGenerator_DefaultPath(t *testing.T) {
}
}
// Test: Domain-only entry (no internal domain pair)
func TestGenerate_DomainOnly(t *testing.T) {
// Test: Exact-match domain produces host-record=
func TestGenerate_ExactMatchDomain(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
globalCfg := &config.State{}
out := g.Generate(globalCfg, []DNSEntry{entry("my-api.payne.io", "192.168.8.151")})
if !strings.Contains(out, "address=/my-api.payne.io/192.168.8.151") {
t.Errorf("expected address entry for domain, got:\n%s", out)
if !strings.Contains(out, "host-record=my-api.payne.io,192.168.8.151") {
t.Errorf("expected host-record entry for domain, got:\n%s", out)
}
if !strings.Contains(out, "local=/my-api.payne.io/") {
t.Errorf("expected local=/ entry for domain, got:\n%s", out)
if strings.Contains(out, "address=/") {
t.Errorf("exact-match domain must not produce address=/ directive:\n%s", out)
}
// Must NOT have empty entries
if strings.Contains(out, "local=//") {
t.Errorf("unexpected empty local=// in output:\n%s", out)
}
if strings.Contains(out, "address=//") {
t.Errorf("unexpected empty address=// in output:\n%s", out)
if strings.Contains(out, "local=/") {
t.Errorf("must not produce local=/ directive:\n%s", out)
}
}
// Test: All domains get local=/ to prevent AAAA leaking upstream
func TestGenerate_AllDomainsGetLocal(t *testing.T) {
// Test: Wildcard domain produces address=/ without local=/
func TestGenerate_WildcardDomain(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
globalCfg := &config.State{}
out := g.Generate(globalCfg, []DNSEntry{wildcardEntry("cloud.payne.io", "192.168.8.240")})
if !strings.Contains(out, "address=/cloud.payne.io/192.168.8.240") {
t.Errorf("expected address=/ entry for wildcard domain, got:\n%s", out)
}
if strings.Contains(out, "host-record=") {
t.Errorf("wildcard domain must not produce host-record= directive:\n%s", out)
}
if strings.Contains(out, "local=/") {
t.Errorf("must not produce local=/ directive:\n%s", out)
}
}
// Test: No local=/ directives are ever generated
func TestGenerate_NoLocalDirectives(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
globalCfg := &config.State{}
// Both "internal" and "public" domains should get local=/ —
// prevents AAAA queries from leaking to upstream DNS (Happy Eyeballs).
entries := []DNSEntry{
entry("internal-api.payne.io", "192.168.8.151"),
entry("cloud.payne.io", "192.168.8.240"),
wildcardEntry("cloud.payne.io", "192.168.8.240"),
}
out := g.Generate(globalCfg, entries)
if !strings.Contains(out, "local=/internal-api.payne.io/") {
t.Errorf("expected local=/ for internal domain, got:\n%s", out)
}
if !strings.Contains(out, "local=/cloud.payne.io/") {
t.Errorf("expected local=/ for public domain (prevents AAAA leaking), got:\n%s", out)
if strings.Contains(out, "local=/") {
t.Errorf("must never produce local=/ directives:\n%s", out)
}
}
// Test: Mix of domains each get their own entries
// Test: filter-AAAA appears in generated config
func TestGenerate_FilterAAAA(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
out := g.Generate(nil, nil)
if !strings.Contains(out, "filter-AAAA") {
t.Errorf("expected filter-AAAA in config, got:\n%s", out)
}
}
// Test: Mix of exact and wildcard domains
func TestGenerate_MixedDomains(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
globalCfg := &config.State{}
entries := []DNSEntry{
entry("cloud.payne.io", "192.168.8.240"), // k8s passthrough
entry("central.payne.io", "192.168.8.151"), // Central HTTP
entry("wild-cloud.payne.io", "192.168.8.151"), // service HTTP
wildcardEntry("cloud.payne.io", "192.168.8.240"), // wildcard
entry("central.payne.io", "192.168.8.151"), // exact
entry("wild-cloud.payne.io", "192.168.8.151"), // exact
}
out := g.Generate(globalCfg, entries)
if !strings.Contains(out, "address=/cloud.payne.io/192.168.8.240") {
t.Errorf("expected address for cloud domain, got:\n%s", out)
t.Errorf("expected address=/ for wildcard domain, got:\n%s", out)
}
if !strings.Contains(out, "address=/central.payne.io/192.168.8.151") {
t.Errorf("expected address for central, got:\n%s", out)
if !strings.Contains(out, "host-record=central.payne.io,192.168.8.151") {
t.Errorf("expected host-record for exact domain, got:\n%s", out)
}
if !strings.Contains(out, "address=/wild-cloud.payne.io/192.168.8.151") {
t.Errorf("expected address for wild-cloud, got:\n%s", out)
if !strings.Contains(out, "host-record=wild-cloud.payne.io,192.168.8.151") {
t.Errorf("expected host-record for exact domain, got:\n%s", out)
}
if strings.Contains(out, "local=//") {
t.Errorf("unexpected empty local=// in output:\n%s", out)
if strings.Contains(out, "local=/") {
t.Errorf("must not produce local=/ directives:\n%s", out)
}
}
// Test: empty entries list produces base config with no address= entries
// Test: empty entries list produces base config with no DNS entries
func TestGenerate_EmptyEntries(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
globalCfg := &config.State{}
@@ -282,15 +300,15 @@ func TestGenerate_EmptyEntries(t *testing.T) {
if !strings.Contains(out, "server=1.1.1.1") {
t.Errorf("expected upstream DNS server, got:\n%s", out)
}
if strings.Contains(out, "address=/") {
t.Errorf("expected no address= entries with empty entries, got:\n%s", out)
if strings.Contains(out, "host-record=") {
t.Errorf("expected no host-record entries with empty entries, got:\n%s", out)
}
if strings.Contains(out, "local=/") {
t.Errorf("expected no local=/ entries with empty entries, got:\n%s", out)
if strings.Contains(out, "address=/") {
t.Errorf("expected no address=/ entries with empty entries, got:\n%s", out)
}
}
// Test: multiple entries each get their own DNS records
// Test: multiple exact-match entries each get their own DNS records
func TestGenerate_MultipleEntries(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
globalCfg := &config.State{}
@@ -304,11 +322,8 @@ func TestGenerate_MultipleEntries(t *testing.T) {
out := g.Generate(globalCfg, entries)
for _, e := range entries {
if !strings.Contains(out, fmt.Sprintf("address=/%s/%s", e.Domain, e.IP)) {
t.Errorf("expected address entry for %s, got:\n%s", e.Domain, out)
}
if !strings.Contains(out, fmt.Sprintf("local=/%s/", e.Domain)) {
t.Errorf("expected local=/ entry for %s, got:\n%s", e.Domain, out)
if !strings.Contains(out, fmt.Sprintf("host-record=%s,%s", e.Domain, e.IP)) {
t.Errorf("expected host-record entry for %s, got:\n%s", e.Domain, out)
}
}
}
@@ -377,7 +392,7 @@ func TestGenerate_ConfiguredDnsmasqIP(t *testing.T) {
}
}
// Test: verify no address=// or local=// entries appear
// Test: verify no leaked empty entries appear
func TestGenerate_NoLeakedEmptyEntries(t *testing.T) {
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
cfg := &config.State{}
@@ -387,7 +402,8 @@ func TestGenerate_NoLeakedEmptyEntries(t *testing.T) {
entries []DNSEntry
}{
{"empty", nil},
{"domain_only", []DNSEntry{entry("example.com", "10.0.0.1")}},
{"exact_domain", []DNSEntry{entry("example.com", "10.0.0.1")}},
{"wildcard_domain", []DNSEntry{wildcardEntry("example.com", "10.0.0.1")}},
{"no_ip", []DNSEntry{entry("example.com", "")}},
{"no_domain", []DNSEntry{entry("", "10.0.0.1")}},
}
@@ -395,11 +411,14 @@ func TestGenerate_NoLeakedEmptyEntries(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out := g.Generate(cfg, tc.entries)
if strings.Contains(out, "host-record=,") {
t.Errorf("leaked empty host-record in output:\n%s", out)
}
if strings.Contains(out, "address=//") {
t.Errorf("leaked empty address=// in output:\n%s", out)
}
if strings.Contains(out, "local=//") {
t.Errorf("leaked empty local=// in output:\n%s", out)
if strings.Contains(out, "local=/") {
t.Errorf("must not produce local=/ directives:\n%s", out)
}
})
}