package rfc2136 import ( "strings" "testing" "github.com/coredns/caddy" "github.com/miekg/dns" ) // A 32-byte HMAC-SHA256 secret, base64-encoded. Generated once and // re-used across tests so failures are reproducible. NEVER use this // value in production — it's literally in this file in plaintext. const testSecret = "xTgset4zj7kHqniSslYFn+OcdCf419olek9MNmOvlUM=" // TestParse exercises the Corefile parser across the matrix of valid // and invalid configurations. Each row is fully self-contained. func TestParse(t *testing.T) { tests := []struct { name string input string shouldErr bool errMatch string // substring to look for in the error (when shouldErr) check func(t *testing.T, p *RFC2136) }{ { name: "single zone, no block", input: `rfc2136 auth.example.com.`, check: func(t *testing.T, p *RFC2136) { wantZone(t, p, "auth.example.com.") if p.TTL != DefaultTTL { t.Errorf("TTL = %d, want default %d", p.TTL, DefaultTTL) } if len(p.TSIGKeys) != 0 { t.Errorf("expected 0 TSIG keys, got %d", len(p.TSIGKeys)) } }, }, { name: "multiple zones in one directive", input: `rfc2136 auth.example.com. acme.example.org.`, check: func(t *testing.T, p *RFC2136) { if len(p.Zones) != 2 { t.Fatalf("Zones = %v, want 2", p.Zones) } wantZone(t, p, "auth.example.com.") wantZone(t, p, "acme.example.org.") }, }, { name: "full block with all directives", input: `rfc2136 auth.example.com. { tsig-key acme-key. hmac-sha256 ` + testSecret + ` ttl 120 persist /var/lib/coredns/rfc2136/auth.db }`, check: func(t *testing.T, p *RFC2136) { wantZone(t, p, "auth.example.com.") if p.TTL != 120 { t.Errorf("TTL = %d, want 120", p.TTL) } if p.PersistPath != "/var/lib/coredns/rfc2136/auth.db" { t.Errorf("PersistPath = %q", p.PersistPath) } k, ok := p.TSIGKeys["acme-key."] if !ok { t.Fatalf("acme-key. not in TSIGKeys; have keys=%v", keysOf(p.TSIGKeys)) } if k.Algorithm != dns.HmacSHA256 { t.Errorf("Algorithm = %q, want %q", k.Algorithm, dns.HmacSHA256) } if len(k.Secret) != 32 { t.Errorf("Secret length = %d, want 32 (decoded SHA-256-sized)", len(k.Secret)) } }, }, { name: "tsig-key name normalised to trailing-dot lowercase", input: `rfc2136 auth.example.com. { tsig-key Acme-Key hmac-sha256 ` + testSecret + ` }`, check: func(t *testing.T, p *RFC2136) { if _, ok := p.TSIGKeys["acme-key."]; !ok { t.Errorf("expected canonicalised key 'acme-key.', got keys=%v", keysOf(p.TSIGKeys)) } }, }, { name: "multiple tsig-keys allowed", input: `rfc2136 auth.example.com. { tsig-key key-a. hmac-sha256 ` + testSecret + ` tsig-key key-b. hmac-sha512 ` + testSecret + ` }`, check: func(t *testing.T, p *RFC2136) { if len(p.TSIGKeys) != 2 { t.Errorf("expected 2 keys, got %d (%v)", len(p.TSIGKeys), keysOf(p.TSIGKeys)) } if p.TSIGKeys["key-a."].Algorithm != dns.HmacSHA256 { t.Errorf("key-a algorithm wrong") } if p.TSIGKeys["key-b."].Algorithm != dns.HmacSHA512 { t.Errorf("key-b algorithm wrong") } }, }, // ─── error cases ────────────────────────────────────────── { name: "no zone", input: `rfc2136`, shouldErr: true, errMatch: "Wrong argument count", }, { name: "unknown directive", input: `rfc2136 auth.example.com. { bogus value }`, shouldErr: true, errMatch: "unknown directive", }, { name: "tsig-key with too few args", input: `rfc2136 auth.example.com. { tsig-key only-name }`, shouldErr: true, errMatch: "tsig-key requires 3 args", }, { name: "unsupported TSIG algorithm", input: `rfc2136 auth.example.com. { tsig-key key. hmac-md5 ` + testSecret + ` }`, shouldErr: true, errMatch: "unsupported TSIG algorithm", }, { name: "malformed base64 secret", input: `rfc2136 auth.example.com. { tsig-key key. hmac-sha256 not_base64_at_all!!! }`, shouldErr: true, errMatch: "invalid base64", }, { name: "secret too short after decode", input: `rfc2136 auth.example.com. { tsig-key key. hmac-sha256 c2hvcnQ= }`, // "short" → 5 bytes < 8 min shouldErr: true, errMatch: "too short", }, { name: "duplicate tsig-key name", input: `rfc2136 auth.example.com. { tsig-key dup. hmac-sha256 ` + testSecret + ` tsig-key dup. hmac-sha512 ` + testSecret + ` }`, shouldErr: true, errMatch: "duplicate tsig-key", }, { name: "nameserver directive overrides default", input: `rfc2136 auth.example.com. { nameserver dns.example.com. }`, check: func(t *testing.T, p *RFC2136) { if p.Nameserver != "dns.example.com." { t.Errorf("Nameserver = %q, want dns.example.com.", p.Nameserver) } }, }, { name: "default nameserver is first zone apex", input: `rfc2136 auth.example.com.`, check: func(t *testing.T, p *RFC2136) { if p.Nameserver != "auth.example.com." { t.Errorf("default Nameserver = %q, want auth.example.com.", p.Nameserver) } }, }, { name: "store is initialised even with no records", input: `rfc2136 auth.example.com.`, check: func(t *testing.T, p *RFC2136) { if p.store == nil { t.Errorf("store should be initialised by parse()") } }, }, { name: "ttl non-numeric", input: `rfc2136 auth.example.com. { ttl not-a-number }`, shouldErr: true, errMatch: "ttl must be a non-negative integer", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := caddy.NewTestController("dns", tt.input) p, err := parse(c) if (err != nil) != tt.shouldErr { t.Fatalf("parse() err = %v, shouldErr = %v", err, tt.shouldErr) } if err != nil { if tt.errMatch != "" && !strings.Contains(err.Error(), tt.errMatch) { t.Errorf("err = %q, expected substring %q", err.Error(), tt.errMatch) } return } if tt.check != nil { tt.check(t, p) } }) } } // wantZone asserts that p.Zones contains the expected canonical zone. func wantZone(t *testing.T, p *RFC2136, want string) { t.Helper() for _, z := range p.Zones { if z == want { return } } t.Errorf("zone %q not in p.Zones=%v", want, p.Zones) } // keysOf is a debug helper for failure messages. func keysOf(m map[string]tsigKey) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out }