mirror of
https://github.com/coredns/coredns.git
synced 2026-10-12 05:25:21 -04:00
plugin/dynupdate: add durable authenticated RFC 2136 updates (#8520)
* plugin/dynupdate: add authenticated RFC 2136 updates Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: fix README test fixtures Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * test: format README fixture map Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: persist updates and bound writable zones Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: preserve middleware and fix interoperability fixtures Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * test(dynupdate): validate Kea lifecycle and bounded zone costs Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: reject duplicate directives and harden client fixtures Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/dynupdate: fix update routing and startup validation Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> --------- Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
318
test/dynupdate_kea_test.go
Normal file
318
test/dynupdate_kea_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Exercise Kea's actual RFC 4703 state machine. Only the lease-change
|
||||
// notifications are synthetic; Kea constructs and signs every DNS UPDATE.
|
||||
func TestDynUpdateKeaLifecycle(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Kea DHCP-DDNS is not available on Windows")
|
||||
}
|
||||
kea, err := exec.LookPath("kea-dhcp-ddns")
|
||||
if err != nil {
|
||||
t.Skip("Kea DHCP-DDNS is not installed")
|
||||
}
|
||||
version, err := exec.Command(kea, "-v").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("Kea version: %v: %s", err, version)
|
||||
}
|
||||
t.Logf("Kea DHCP-DDNS %s", strings.TrimSpace(string(version)))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name, address, conflictAddress, reverse string
|
||||
rrType uint16
|
||||
}{
|
||||
{"v4", "192.0.2.10", "192.0.2.11", "2.0.192.in-addr.arpa.", dns.TypeA},
|
||||
{"v6", "2001:db8::10", "2001:db8::11", "8.b.d.0.1.0.0.2.ip6.arpa.", dns.TypeAAAA},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
var corefile strings.Builder
|
||||
for i, zone := range []string{"example.org.", tc.reverse} {
|
||||
seed := filepath.Join(dir, fmt.Sprintf("zone-%d", i))
|
||||
data := fmt.Sprintf("%s 60 IN SOA ns.example.org. hostmaster.example.org. 10 60 60 60 60\n%s 60 IN NS ns.example.org.\n", zone, zone)
|
||||
if err := os.WriteFile(seed, []byte(data), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grants := "A AAAA DHCID ANY"
|
||||
if i == 1 {
|
||||
grants = "PTR DHCID ANY"
|
||||
}
|
||||
fmt.Fprintf(&corefile, `%s:0 {
|
||||
bind 127.0.0.1
|
||||
cache
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
dynupdate {
|
||||
file %q
|
||||
database %q
|
||||
allow %s * %s
|
||||
}
|
||||
}
|
||||
`, zone, dynUpdateKey, dynUpdateSecret, seed, seed+".db", dynUpdateKey, grants)
|
||||
}
|
||||
s, addr, _, err := CoreDNSServerAndPorts(corefile.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stopDynUpdateServer(t, s)
|
||||
send := startKeaDynUpdate(t, kea, addr, tc.reverse)
|
||||
client := &dns.Client{Net: "udp", Timeout: time.Second}
|
||||
owner := tc.name + ".example.org."
|
||||
ptr, err := dns.ReverseAddr(tc.address)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conflictPTR, err := dns.ReverseAddr(tc.conflictAddress)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256([]byte("client-" + tc.name))
|
||||
dhcid := append([]byte{0, 1, 1}, digest[:]...)
|
||||
check := func(name string, rrType uint16, ttl uint32, value string) {
|
||||
t.Helper()
|
||||
query := new(dns.Msg).SetQuestion(name, rrType)
|
||||
r := exchangeDynUpdate(t, client, addr, query, dns.RcodeSuccess)
|
||||
want, err := dns.NewRR(fmt.Sprintf("%s %d IN %s %s", name, ttl, dns.TypeToString[rrType], value))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl == 0 && len(r.Answer) == 1 {
|
||||
want.Header().Ttl = r.Answer[0].Header().Ttl
|
||||
}
|
||||
if !r.Authoritative || len(r.Answer) != 1 || r.Answer[0].String() != want.String() {
|
||||
t.Fatalf("expected %s, got %v", want, r)
|
||||
}
|
||||
}
|
||||
checkLease := func(ttl uint32) {
|
||||
t.Helper()
|
||||
check(owner, tc.rrType, ttl, tc.address)
|
||||
// Renewals replace addresses, but need not replace the ownership RR.
|
||||
check(owner, dns.TypeDHCID, 0, base64.StdEncoding.EncodeToString(dhcid))
|
||||
check(ptr, dns.TypePTR, ttl, owner)
|
||||
}
|
||||
serials := func() []uint32 {
|
||||
t.Helper()
|
||||
var result []uint32
|
||||
for _, zone := range []string{"example.org.", tc.reverse} {
|
||||
r := exchangeDynUpdate(t, client, addr, new(dns.Msg).SetQuestion(zone, dns.TypeSOA), dns.RcodeSuccess)
|
||||
if len(r.Answer) != 1 {
|
||||
t.Fatalf("missing SOA: %v", r)
|
||||
}
|
||||
result = append(result, r.Answer[0].(*dns.SOA).Serial)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Populate negative cache entries before claiming the name.
|
||||
for _, name := range []string{owner, ptr} {
|
||||
exchangeDynUpdate(t, client, addr, new(dns.Msg).SetQuestion(name, dns.TypeANY), dns.RcodeNameError)
|
||||
}
|
||||
send(0, owner, tc.address, dhcid, 60, "DHCP_DDNS_ADD_SUCCEEDED")
|
||||
checkLease(60)
|
||||
send(0, owner, tc.address, dhcid, 120, "DHCP_DDNS_ADD_SUCCEEDED")
|
||||
checkLease(120)
|
||||
|
||||
before := serials()
|
||||
other := append([]byte(nil), dhcid...)
|
||||
other[len(other)-1] ^= 1
|
||||
send(0, owner, tc.conflictAddress, other, 60, "DHCP_DDNS_ADD_FAILED")
|
||||
checkLease(120)
|
||||
exchangeDynUpdate(t, client, addr, new(dns.Msg).SetQuestion(conflictPTR, dns.TypePTR), dns.RcodeNameError)
|
||||
after := serials()
|
||||
if before[0] != after[0] || before[1] != after[1] {
|
||||
t.Fatalf("conflicting client changed zone serials: %v -> %v", before, after)
|
||||
}
|
||||
|
||||
send(1, owner, tc.address, dhcid, 120, "DHCP_DDNS_REMOVE_SUCCEEDED")
|
||||
for _, name := range []string{owner, ptr} {
|
||||
exchangeDynUpdate(t, client, addr, new(dns.Msg).SetQuestion(name, dns.TypeANY), dns.RcodeNameError)
|
||||
}
|
||||
// A different client may claim the name only after the old owner releases it.
|
||||
send(0, owner, tc.conflictAddress, other, 60, "DHCP_DDNS_ADD_SUCCEEDED")
|
||||
check(owner, tc.rrType, 60, tc.conflictAddress)
|
||||
check(owner, dns.TypeDHCID, 60, base64.StdEncoding.EncodeToString(other))
|
||||
check(conflictPTR, dns.TypePTR, 60, owner)
|
||||
send(1, owner, tc.conflictAddress, other, 60, "DHCP_DDNS_REMOVE_SUCCEEDED")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func startKeaDynUpdate(t *testing.T, executable, dnsAddr, reverseZone string) func(int, string, string, []byte, uint32, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ncrAddr := pc.LocalAddr().String()
|
||||
ncrPort := pc.LocalAddr().(*net.UDPAddr).Port
|
||||
if err := pc.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(dnsAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dnsPort, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
domain := func(zone string) map[string]any {
|
||||
return map[string]any{"ddns-domains": []any{map[string]any{
|
||||
"name": zone, "key-name": dynUpdateKey,
|
||||
"dns-servers": []any{map[string]any{"ip-address": host, "port": dnsPort}},
|
||||
}}}
|
||||
}
|
||||
config := map[string]any{"DhcpDdns": map[string]any{
|
||||
"ip-address": "127.0.0.1", "port": ncrPort,
|
||||
"dns-server-timeout": 2000, "ncr-protocol": "UDP", "ncr-format": "JSON",
|
||||
"tsig-keys": []any{map[string]any{"name": dynUpdateKey, "algorithm": "HMAC-SHA256", "secret": dynUpdateSecret}},
|
||||
"forward-ddns": domain("example.org."), "reverse-ddns": domain(reverseZone),
|
||||
"loggers": []any{map[string]any{
|
||||
"name": "kea-dhcp-ddns", "severity": "DEBUG", "debuglevel": 99,
|
||||
"output_options": []any{map[string]any{"output": "stdout", "flush": true}},
|
||||
}},
|
||||
}}
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configDir := dir
|
||||
if root := os.Getenv("COREDNS_KEA_CONFIG_DIR"); root != "" {
|
||||
configDir, err = os.MkdirTemp(root, "coredns-ddns-") //nolint:usetesting // Must use a path permitted by the distribution's AppArmor profile.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(configDir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
// Distribution AppArmor profiles may require this config basename and
|
||||
// fixed runtime directories. CI provisions them without changing the profile.
|
||||
path := filepath.Join(configDir, "kea-dhcp-ddns.conf")
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logfile, err := os.Create(filepath.Join(dir, "kea.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
cmd := exec.CommandContext(ctx, executable, "-c", path)
|
||||
cmd.Env = os.Environ()
|
||||
for _, name := range []string{"KEA_PIDFILE_DIR", "KEA_LOCKFILE_DIR"} {
|
||||
if os.Getenv(name) == "" {
|
||||
cmd.Env = append(cmd.Env, name+"="+dir)
|
||||
}
|
||||
}
|
||||
cmd.Stdout, cmd.Stderr = logfile, logfile
|
||||
if err := cmd.Start(); err != nil {
|
||||
cancel()
|
||||
logfile.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan struct{})
|
||||
var waitErr error
|
||||
go func() {
|
||||
waitErr = cmd.Wait()
|
||||
close(done)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
// Graceful shutdown removes the PID file before the next lifecycle case.
|
||||
_ = cmd.Process.Signal(os.Interrupt)
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
cancel()
|
||||
logfile.Close()
|
||||
if t.Failed() {
|
||||
out, _ := os.ReadFile(logfile.Name())
|
||||
t.Logf("Kea log:\n%s", out)
|
||||
}
|
||||
})
|
||||
waitFor := func(offset int64, event string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
out, err := os.ReadFile(logfile.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if int64(len(out)) >= offset {
|
||||
recent := string(out[offset:])
|
||||
if strings.Contains(recent, event) {
|
||||
return
|
||||
}
|
||||
for _, terminal := range []string{"DHCP_DDNS_ADD_SUCCEEDED", "DHCP_DDNS_ADD_FAILED", "DHCP_DDNS_REMOVE_SUCCEEDED", "DHCP_DDNS_REMOVE_FAILED"} {
|
||||
if strings.Contains(recent, terminal) {
|
||||
t.Fatalf("Kea reported %s while waiting for %s", terminal, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatalf("Kea exited before %s: %v", event, waitErr)
|
||||
default:
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("Kea did not report %s", event)
|
||||
}
|
||||
waitFor(0, "DHCP_DDNS_QUEUE_MGR_STARTED")
|
||||
return func(change int, fqdn, address string, dhcid []byte, ttl uint32, event string) {
|
||||
t.Helper()
|
||||
info, err := logfile.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ncr, err := json.Marshal(map[string]any{
|
||||
"change-type": change, "forward-change": true, "reverse-change": true,
|
||||
"fqdn": fqdn, "ip-address": address, "dhcid": hex.EncodeToString(dhcid),
|
||||
"lease-expires-on": time.Now().UTC().Add(time.Duration(ttl) * time.Second).Format("20060102150405"),
|
||||
"lease-length": ttl, "use-conflict-resolution": true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Kea's UDP NCR framing is a network-order uint16 length followed by JSON.
|
||||
wire := make([]byte, 2+len(ncr))
|
||||
binary.BigEndian.PutUint16(wire, uint16(len(ncr)))
|
||||
copy(wire[2:], ncr)
|
||||
conn, err := net.DialTimeout("udp", ncrAddr, time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if _, err := conn.Write(wire); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor(info.Size(), event)
|
||||
}
|
||||
}
|
||||
379
test/dynupdate_persistence_test.go
Normal file
379
test/dynupdate_persistence_test.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func persistentDynUpdateConfig(t *testing.T, network string) (corefile, seed string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
seed = filepath.Join(dir, "example.org.zone")
|
||||
if err := os.WriteFile(seed, []byte(dynUpdateZone), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, tlsLine := "example.org:0", ""
|
||||
if network == "tcp-tls" {
|
||||
server = "tls://" + server
|
||||
tlsLine = "tls ../plugin/tls/test_cert.pem ../plugin/tls/test_key.pem"
|
||||
}
|
||||
return fmt.Sprintf(`%s {
|
||||
bind 127.0.0.1
|
||||
%s
|
||||
header {
|
||||
response set ra
|
||||
}
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
cache
|
||||
dynupdate {
|
||||
file "%s"
|
||||
database "%s"
|
||||
allow %s * *
|
||||
}
|
||||
}`, server, tlsLine, dynUpdateKey, dynUpdateSecret, filepath.ToSlash(seed), filepath.ToSlash(filepath.Join(dir, "updates.db")), dynUpdateKey), seed
|
||||
}
|
||||
|
||||
func stopDynUpdateServer(t *testing.T, s *caddy.Instance) {
|
||||
t.Helper()
|
||||
if err := s.Stop(); err != nil {
|
||||
t.Errorf("stopping server: %v", err)
|
||||
}
|
||||
for _, err := range s.ShutdownCallbacks() {
|
||||
t.Errorf("shutdown callback: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func exchangeDynUpdate(t *testing.T, client *dns.Client, addr string, m *dns.Msg, code int) *dns.Msg {
|
||||
t.Helper()
|
||||
if m.Opcode == dns.OpcodeUpdate {
|
||||
m.SetTsig(dynUpdateKey, dns.HmacSHA256, 300, time.Now().Unix())
|
||||
}
|
||||
r, _, err := client.Exchange(m, addr)
|
||||
if err != nil || r == nil || r.Rcode != code {
|
||||
t.Fatalf("exchange: response=%v err=%v want=%s", r, err, dns.RcodeToString[code])
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestDynUpdatePersistentWire(t *testing.T) {
|
||||
for _, network := range []string{"udp", "tcp", "tcp-tls"} {
|
||||
t.Run(network, func(t *testing.T) {
|
||||
corefile, seed := persistentDynUpdateConfig(t, network)
|
||||
s, udp, tcp, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if s != nil {
|
||||
stopDynUpdateServer(t, s)
|
||||
}
|
||||
}()
|
||||
addr := tcp
|
||||
if network == "udp" {
|
||||
addr = udp
|
||||
}
|
||||
client := &dns.Client{
|
||||
Net: network, Timeout: 5 * time.Second,
|
||||
TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret},
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: true}, // test certificate
|
||||
}
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion("new.example.org.", dns.TypeTXT)
|
||||
exchangeDynUpdate(t, client, addr, query, dns.RcodeNameError)
|
||||
query.SetQuestion("www.example.org.", dns.TypeA)
|
||||
exchangeDynUpdate(t, client, addr, query, dns.RcodeSuccess)
|
||||
types := []string{
|
||||
`www.example.org. 120 IN A 192.0.2.100`,
|
||||
`www.example.org. 120 IN AAAA 2001:db8::100`,
|
||||
`new.example.org. 120 IN TXT "durable"`,
|
||||
`ptr.example.org. 120 IN PTR www.example.org.`,
|
||||
`_service._tcp.example.org. 120 IN SRV 0 0 443 www.example.org.`,
|
||||
`www.example.org. 120 IN DHCID AAEAAQ==`,
|
||||
`example.org. 120 IN CAA 0 issue "ca.example"`,
|
||||
}
|
||||
records := make([]dns.RR, 0, len(types))
|
||||
for _, text := range types {
|
||||
rr, err := dns.NewRR(text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
records = append(records, rr)
|
||||
}
|
||||
update := new(dns.Msg).SetUpdate("example.org.")
|
||||
update.RemoveRRset([]dns.RR{records[0]})
|
||||
update.Insert(records)
|
||||
exchangeDynUpdate(t, client, addr, update, dns.RcodeSuccess)
|
||||
for round := range 2 {
|
||||
for _, rr := range records {
|
||||
query.SetQuestion(rr.Header().Name, rr.Header().Rrtype)
|
||||
r := exchangeDynUpdate(t, client, addr, query, dns.RcodeSuccess)
|
||||
if len(r.Answer) != 1 || r.Answer[0].String() != rr.String() {
|
||||
t.Fatalf("round %d: stale or lost record: %v", round, r)
|
||||
}
|
||||
if !r.RecursionAvailable {
|
||||
t.Fatal("dynamic answer bypassed the configured header plugin")
|
||||
}
|
||||
}
|
||||
query.SetQuestion("example.org.", dns.TypeSOA)
|
||||
soa := exchangeDynUpdate(t, client, addr, query, dns.RcodeSuccess)
|
||||
if len(soa.Answer) != 1 || soa.Answer[0].(*dns.SOA).Serial != 11 {
|
||||
t.Fatalf("wrong SOA serial: %v", soa)
|
||||
}
|
||||
if round == 0 {
|
||||
stopDynUpdateServer(t, s)
|
||||
s = nil
|
||||
if err := os.Remove(seed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, udp, tcp, err = CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr = tcp
|
||||
if network == "udp" {
|
||||
addr = udp
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete after recovery and confirm the positive response cannot stick.
|
||||
update = new(dns.Msg).SetUpdate("example.org.")
|
||||
update.RemoveRRset([]dns.RR{records[2]})
|
||||
exchangeDynUpdate(t, client, addr, update, dns.RcodeSuccess)
|
||||
query.SetQuestion("new.example.org.", dns.TypeTXT)
|
||||
exchangeDynUpdate(t, client, addr, query, dns.RcodeNameError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateFailedStartupDoesNotCreateDatabase(t *testing.T) {
|
||||
for _, failure := range []string{"directive", "listener"} {
|
||||
t.Run(failure, func(t *testing.T) {
|
||||
corefile, seed := persistentDynUpdateConfig(t, "udp")
|
||||
bad := strings.Replace(corefile, "\n\t\tcache", "\n\t\tfile\n\t\tcache", 1)
|
||||
if failure == "listener" {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
bad = strings.Replace(corefile, "example.org:0", fmt.Sprintf("example.org:%d", ln.Addr().(*net.TCPAddr).Port), 1)
|
||||
}
|
||||
if s, err := CoreDNSServer(bad); err == nil {
|
||||
stopDynUpdateServer(t, s)
|
||||
t.Fatal("invalid startup unexpectedly succeeded")
|
||||
}
|
||||
database := filepath.Join(filepath.Dir(seed), "updates.db")
|
||||
if _, err := os.Stat(database); !os.IsNotExist(err) {
|
||||
t.Errorf("failed startup created a database: %v", err)
|
||||
}
|
||||
updatedSeed := strings.Replace(dynUpdateZone, "10 60 60 60 60", "20 60 60 60 60", 1)
|
||||
if err := os.WriteFile(seed, []byte(updatedSeed), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, udp, _, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatalf("retrying startup: %v", err)
|
||||
}
|
||||
defer stopDynUpdateServer(t, s)
|
||||
query := new(dns.Msg).SetQuestion("example.org.", dns.TypeSOA)
|
||||
r := exchangeDynUpdate(t, &dns.Client{Net: "udp"}, udp, query, dns.RcodeSuccess)
|
||||
if len(r.Answer) != 1 || r.Answer[0].(*dns.SOA).Serial != 20 {
|
||||
t.Fatalf("retry reused data from failed startup: %v", r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateRejectsDuplicateDirective(t *testing.T) {
|
||||
corefile, seed := persistentDynUpdateConfig(t, "udp")
|
||||
duplicate := fmt.Sprintf(`
|
||||
dynupdate example.org. {
|
||||
file "%s"
|
||||
allow %s restricted.example.org. TXT
|
||||
}
|
||||
`, filepath.ToSlash(seed), dynUpdateKey)
|
||||
corefile = strings.Replace(corefile, "\n\t\tcache", duplicate+"\n\t\tcache", 1)
|
||||
if s, err := CoreDNSServer(corefile); err == nil {
|
||||
stopDynUpdateServer(t, s)
|
||||
t.Fatal("duplicate dynupdate directive was silently accepted")
|
||||
} else if !strings.Contains(err.Error(), "can only be used once") {
|
||||
t.Fatalf("unexpected startup failure: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateCorefileReload(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Caddy listener file-descriptor inheritance is unavailable on Windows")
|
||||
}
|
||||
corefile, _ := persistentDynUpdateConfig(t, "udp")
|
||||
s, udp, _, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { stopDynUpdateServer(t, s) }()
|
||||
client := &dns.Client{Net: "udp", TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret}}
|
||||
rr, err := dns.NewRR(`reload.example.org. 60 IN TXT "survives"`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
update := new(dns.Msg).SetUpdate("example.org.")
|
||||
update.Insert([]dns.RR{rr})
|
||||
exchangeDynUpdate(t, client, udp, update, dns.RcodeSuccess)
|
||||
// file is initialized after dynupdate. An invalid directive must leave the old server
|
||||
// and database usable, without retaining a ref for the abandoned config.
|
||||
bad := strings.Replace(corefile, "\n\t\tcache", "\n\t\tfile\n\t\tcache", 1)
|
||||
if next, err := s.Restart(NewInput(bad)); err == nil {
|
||||
s = next
|
||||
t.Fatal("invalid reload unexpectedly succeeded")
|
||||
}
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion(rr.Header().Name, dns.TypeTXT)
|
||||
exchangeDynUpdate(t, client, udp, query, dns.RcodeSuccess)
|
||||
next, err := s.Restart(NewInput(corefile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s = next
|
||||
udp, _ = CoreDNSServerPorts(s, 0)
|
||||
r := exchangeDynUpdate(t, client, udp, query, dns.RcodeSuccess)
|
||||
if len(r.Answer) != 1 || r.Answer[0].String() != rr.String() {
|
||||
t.Fatalf("reload lost acknowledged update: %v", r)
|
||||
}
|
||||
update = new(dns.Msg).SetUpdate("example.org.")
|
||||
update.RemoveRRset([]dns.RR{rr})
|
||||
exchangeDynUpdate(t, client, udp, update, dns.RcodeSuccess)
|
||||
exchangeDynUpdate(t, client, udp, query, dns.RcodeNameError)
|
||||
}
|
||||
|
||||
func TestDynUpdateNsupdate(t *testing.T) {
|
||||
nsupdate, err := exec.LookPath("nsupdate")
|
||||
if err != nil {
|
||||
t.Skip("BIND nsupdate is not installed")
|
||||
}
|
||||
corefile, _ := persistentDynUpdateConfig(t, "udp")
|
||||
s, udp, tcpAddr, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stopDynUpdateServer(t, s)
|
||||
for _, tcp := range []bool{false, true} {
|
||||
addr := udp
|
||||
if tcp {
|
||||
addr = tcpAddr
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{"-y", "hmac-sha256:" + dynUpdateKey + ":" + dynUpdateSecret}
|
||||
if tcp {
|
||||
args = append(args, "-v")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, nsupdate, args...)
|
||||
cmd.Stdin = strings.NewReader(fmt.Sprintf(`server %s %s
|
||||
zone example.org.
|
||||
prereq nxdomain nsupdate.example.org.
|
||||
update add nsupdate.example.org. 60 TXT "interop"
|
||||
send
|
||||
prereq yxrrset nsupdate.example.org. TXT "interop"
|
||||
update delete nsupdate.example.org. TXT
|
||||
send
|
||||
`, host, port))
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("nsupdate tcp=%v: %v\n%s", tcp, err, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateCoalescesNotify(t *testing.T) {
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
blocked := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var unblock sync.Once
|
||||
defer unblock.Do(func() { close(release) })
|
||||
notifications := make(chan struct{}, 2)
|
||||
var calls atomic.Int32
|
||||
secondary := &dns.Server{PacketConn: pc, Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
if r.Opcode != dns.OpcodeNotify || len(r.Question) != 1 || r.Question[0].Name != "example.org." {
|
||||
t.Errorf("unexpected notification: %v", r)
|
||||
}
|
||||
if calls.Add(1) == 1 {
|
||||
close(blocked)
|
||||
<-release
|
||||
}
|
||||
if err := w.WriteMsg(new(dns.Msg).SetReply(r)); err != nil {
|
||||
t.Errorf("replying to NOTIFY: %v", err)
|
||||
}
|
||||
select {
|
||||
case notifications <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})}
|
||||
stopped := make(chan error, 1)
|
||||
go func() { stopped <- secondary.ActivateAndServe() }()
|
||||
defer func() {
|
||||
unblock.Do(func() { close(release) })
|
||||
secondary.Shutdown()
|
||||
if err := <-stopped; err != nil {
|
||||
t.Errorf("notification server: %v", err)
|
||||
}
|
||||
}()
|
||||
corefile, _ := persistentDynUpdateConfig(t, "udp")
|
||||
corefile = strings.Replace(corefile, "\n\t\tcache", fmt.Sprintf("\n\t\ttransfer {\n\t\t\tto %s\n\t\t}\n\t\tcache", pc.LocalAddr()), 1)
|
||||
s, udp, _, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stopDynUpdateServer(t, s)
|
||||
client := &dns.Client{Net: "udp", TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret}}
|
||||
for i := range 4 {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("notify-%d.example.org. 60 IN A 192.0.2.%d", i, i+1))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
update := new(dns.Msg).SetUpdate("example.org.")
|
||||
update.Insert([]dns.RR{rr})
|
||||
exchangeDynUpdate(t, client, udp, update, dns.RcodeSuccess)
|
||||
if i == 0 {
|
||||
select {
|
||||
case <-blocked:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("committed update did not trigger NOTIFY")
|
||||
}
|
||||
}
|
||||
}
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Fatalf("concurrent NOTIFY operations: %d", got)
|
||||
}
|
||||
unblock.Do(func() { close(release) })
|
||||
for range 2 {
|
||||
select {
|
||||
case <-notifications:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("pending notification was lost")
|
||||
}
|
||||
}
|
||||
}
|
||||
357
test/dynupdate_test.go
Normal file
357
test/dynupdate_test.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
plugintest "github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
dynUpdateKey = "update-key.example.org."
|
||||
dynUpdateSecret = "i9M+00yrECfVZG2qCjr4mPpaGim/Bq+IWMiNrLjUO4Y="
|
||||
)
|
||||
|
||||
const dynUpdateZone = `$ORIGIN example.org.
|
||||
@ 60 IN SOA ns.example.org. hostmaster.example.org. 10 60 60 60 60
|
||||
@ 60 IN NS ns.example.org.
|
||||
ns 60 IN A 192.0.2.53
|
||||
www 60 IN A 192.0.2.1
|
||||
`
|
||||
|
||||
func TestDynUpdateUDPAndTCP(t *testing.T) {
|
||||
zoneFile, remove, err := plugintest.TempFile(".", dynUpdateZone)
|
||||
if err != nil {
|
||||
t.Fatalf("creating zone file: %v", err)
|
||||
}
|
||||
defer remove()
|
||||
|
||||
corefile := fmt.Sprintf(`example.org:0 {
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
dynupdate {
|
||||
file %s
|
||||
allow %s * TXT
|
||||
}
|
||||
}`, dynUpdateKey, dynUpdateSecret, zoneFile, dynUpdateKey)
|
||||
server, udp, tcp, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatalf("starting CoreDNS: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
net string
|
||||
addr string
|
||||
}{
|
||||
{name: "udp", net: "udp", addr: udp},
|
||||
{name: "tcp", net: "tcp", addr: tcp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
owner := tc.name + ".example.org."
|
||||
rr, err := dns.NewRR(owner + ` 60 IN TXT "dynamic"`)
|
||||
if err != nil {
|
||||
t.Fatalf("creating update RR: %v", err)
|
||||
}
|
||||
|
||||
msg := new(dns.Msg).SetUpdate("example.org.")
|
||||
msg.Insert([]dns.RR{rr})
|
||||
msg.SetTsig(dynUpdateKey, dns.HmacSHA256, 300, time.Now().Unix())
|
||||
client := &dns.Client{Net: tc.net, TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret}}
|
||||
resp, _, err := client.Exchange(msg, tc.addr)
|
||||
if err != nil {
|
||||
t.Fatalf("sending %s UPDATE: %v", tc.name, err)
|
||||
}
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("UPDATE rcode = %s, want NOERROR", dns.RcodeToString[resp.Rcode])
|
||||
}
|
||||
if resp.Opcode != dns.OpcodeUpdate {
|
||||
t.Fatalf("response opcode = %d, want UPDATE", resp.Opcode)
|
||||
}
|
||||
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion(owner, dns.TypeTXT)
|
||||
answer, _, err := (&dns.Client{Net: "udp"}).Exchange(query, udp)
|
||||
if err != nil {
|
||||
t.Fatalf("querying updated record: %v", err)
|
||||
}
|
||||
if answer.Rcode != dns.RcodeSuccess || len(answer.Answer) != 1 {
|
||||
t.Fatalf("updated query response = rcode %s, %d answers", dns.RcodeToString[answer.Rcode], len(answer.Answer))
|
||||
}
|
||||
if got := answer.Answer[0].String(); got != owner+"\t60\tIN\tTXT\t\"dynamic\"" {
|
||||
t.Fatalf("updated record = %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateRejectsUnsignedRequest(t *testing.T) {
|
||||
zoneFile, remove, err := plugintest.TempFile(".", dynUpdateZone)
|
||||
if err != nil {
|
||||
t.Fatalf("creating zone file: %v", err)
|
||||
}
|
||||
defer remove()
|
||||
|
||||
corefile := fmt.Sprintf(`example.org:0 {
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
dynupdate {
|
||||
file %s
|
||||
allow %s * TXT
|
||||
}
|
||||
}`, dynUpdateKey, dynUpdateSecret, zoneFile, dynUpdateKey)
|
||||
server, udp, _, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatalf("starting CoreDNS: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
msg := new(dns.Msg).SetUpdate("example.org.")
|
||||
rr, err := dns.NewRR("unsigned.example.org. 60 IN TXT \"denied\"")
|
||||
if err != nil {
|
||||
t.Fatalf("creating update RR: %v", err)
|
||||
}
|
||||
msg.Insert([]dns.RR{rr})
|
||||
resp, err := dns.Exchange(msg, udp)
|
||||
if err != nil {
|
||||
t.Fatalf("sending unsigned UPDATE: %v", err)
|
||||
}
|
||||
if resp.Rcode != dns.RcodeRefused {
|
||||
t.Fatalf("unsigned UPDATE rcode = %s, want REFUSED", dns.RcodeToString[resp.Rcode])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateRejectsOtherZones(t *testing.T) {
|
||||
seed, removeSeed, err := plugintest.TempFile(".", dynUpdateZone)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeSeed()
|
||||
other, removeOther, err := plugintest.TempFile(".", strings.ReplaceAll(dynUpdateZone, "example.org.", "other.example."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeOther()
|
||||
corefile := fmt.Sprintf(`.:0 {
|
||||
bind 127.0.0.1
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
dynupdate example.org. {
|
||||
file %s
|
||||
allow %s * TXT
|
||||
}
|
||||
file %s other.example.
|
||||
}`, dynUpdateKey, dynUpdateSecret, seed, dynUpdateKey, other)
|
||||
s, udp, tcp, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stopDynUpdateServer(t, s)
|
||||
for _, network := range []string{"udp", "tcp"} {
|
||||
t.Run(network, func(t *testing.T) {
|
||||
addr := udp
|
||||
if network == "tcp" {
|
||||
addr = tcp
|
||||
}
|
||||
client := &dns.Client{Net: network, TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret}}
|
||||
query := new(dns.Msg).SetQuestion("other.example.", dns.TypeSOA)
|
||||
r := exchangeDynUpdate(t, client, addr, query, dns.RcodeSuccess)
|
||||
if len(r.Answer) != 1 {
|
||||
t.Fatalf("ordinary query did not reach file: %v", r)
|
||||
}
|
||||
rr, err := dns.NewRR(`new.other.example. 60 IN TXT "must not succeed"`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
update := new(dns.Msg).SetUpdate("other.example.")
|
||||
update.Insert([]dns.RR{rr})
|
||||
update.SetTsig(dynUpdateKey, dns.HmacSHA256, 300, time.Now().Unix())
|
||||
r, _, err = client.Exchange(update, addr)
|
||||
// miekg/dns returns ErrAuth for every NOTAUTH before verifying TSIG.
|
||||
// The nsupdate subtest below also checks the signed response with BIND.
|
||||
if (err != nil && err != dns.ErrAuth) || r == nil || r.Rcode != dns.RcodeNotAuth {
|
||||
t.Fatalf("wrong-zone UPDATE: response=%v err=%v, want NOTAUTH", r, err)
|
||||
}
|
||||
if tsig := r.IsTsig(); tsig == nil || tsig.Error != dns.RcodeSuccess || tsig.MAC == "" {
|
||||
t.Fatalf("wrong-zone rejection lost TSIG: %v", r)
|
||||
}
|
||||
query.SetQuestion(rr.Header().Name, dns.TypeTXT)
|
||||
exchangeDynUpdate(t, client, addr, query, dns.RcodeNameError)
|
||||
t.Run("nsupdate", func(t *testing.T) {
|
||||
nsupdate, err := exec.LookPath("nsupdate")
|
||||
if err != nil {
|
||||
t.Skip("BIND nsupdate is not installed")
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{"-y", "hmac-sha256:" + dynUpdateKey + ":" + dynUpdateSecret}
|
||||
if network == "tcp" {
|
||||
args = append(args, "-v")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, nsupdate, args...)
|
||||
cmd.Stdin = strings.NewReader(fmt.Sprintf("server %s %s\nzone other.example.\nupdate add %s\nsend\n", host, port, rr))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil || !strings.Contains(string(out), "update failed: NOTAUTH") || strings.Contains(strings.ToLower(string(out)), "tsig") {
|
||||
t.Fatalf("nsupdate: err=%v output=%s, want NOTAUTH without a TSIG error", err, out)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateIsNotServedFromCache(t *testing.T) {
|
||||
zoneFile, remove, err := plugintest.TempFile(".", dynUpdateZone)
|
||||
if err != nil {
|
||||
t.Fatalf("creating zone file: %v", err)
|
||||
}
|
||||
defer remove()
|
||||
|
||||
corefile := fmt.Sprintf(`example.org:0 {
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
cache
|
||||
dynupdate {
|
||||
file %s
|
||||
allow %s * TXT
|
||||
}
|
||||
}`, dynUpdateKey, dynUpdateSecret, zoneFile, dynUpdateKey)
|
||||
server, udp, _, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatalf("starting CoreDNS: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
owner := "example.org."
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion(owner, dns.TypeSOA)
|
||||
if resp, _, err := (&dns.Client{Net: "udp"}).Exchange(query, udp); err != nil {
|
||||
t.Fatalf("priming SOA cache: %v", err)
|
||||
} else if resp.Rcode != dns.RcodeSuccess || len(resp.Answer) != 1 {
|
||||
t.Fatalf("priming query response = %s with %d answers, want SOA", dns.RcodeToString[resp.Rcode], len(resp.Answer))
|
||||
}
|
||||
|
||||
rr, err := dns.NewRR("cached.example.org. 60 IN TXT \"dynamic\"")
|
||||
if err != nil {
|
||||
t.Fatalf("creating update RR: %v", err)
|
||||
}
|
||||
update := new(dns.Msg).SetUpdate("example.org.")
|
||||
update.Insert([]dns.RR{rr})
|
||||
update.SetTsig(dynUpdateKey, dns.HmacSHA256, 300, time.Now().Unix())
|
||||
client := &dns.Client{Net: "udp", TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret}}
|
||||
resp, _, err := client.Exchange(update, udp)
|
||||
if err != nil {
|
||||
t.Fatalf("sending UPDATE after cached query: %v", err)
|
||||
}
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("UPDATE rcode after cached query = %s, want NOERROR", dns.RcodeToString[resp.Rcode])
|
||||
}
|
||||
|
||||
// Use a distinct cache key to verify the committed record without being
|
||||
// masked by the SOA entry deliberately primed above.
|
||||
query.SetQuestion("cached.example.org.", dns.TypeTXT)
|
||||
query.CheckingDisabled = true
|
||||
resp, _, err = (&dns.Client{Net: "udp"}).Exchange(query, udp)
|
||||
if err != nil {
|
||||
t.Fatalf("querying updated record: %v", err)
|
||||
}
|
||||
if resp.Rcode != dns.RcodeSuccess || len(resp.Answer) != 1 || resp.Answer[0].String() != "cached.example.org.\t60\tIN\tTXT\t\"dynamic\"" {
|
||||
t.Fatalf("updated response = %#v, want one TXT answer", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynUpdateAXFRIncludesUpdatedRecord(t *testing.T) {
|
||||
zoneFile, remove, err := plugintest.TempFile(".", dynUpdateZone)
|
||||
if err != nil {
|
||||
t.Fatalf("creating zone file: %v", err)
|
||||
}
|
||||
defer remove()
|
||||
|
||||
corefile := fmt.Sprintf(`example.org:0 {
|
||||
tsig {
|
||||
secret %s %s
|
||||
require_opcode UPDATE
|
||||
}
|
||||
transfer {
|
||||
to *
|
||||
}
|
||||
dynupdate {
|
||||
file %s
|
||||
allow %s * TXT
|
||||
}
|
||||
}`, dynUpdateKey, dynUpdateSecret, zoneFile, dynUpdateKey)
|
||||
server, _, tcp, err := CoreDNSServerAndPorts(corefile)
|
||||
if err != nil {
|
||||
t.Fatalf("starting CoreDNS: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
rr, err := dns.NewRR("axfr.example.org. 60 IN TXT \"dynamic\"")
|
||||
if err != nil {
|
||||
t.Fatalf("creating update RR: %v", err)
|
||||
}
|
||||
update := new(dns.Msg).SetUpdate("example.org.")
|
||||
update.Insert([]dns.RR{rr})
|
||||
update.SetTsig(dynUpdateKey, dns.HmacSHA256, 300, time.Now().Unix())
|
||||
client := &dns.Client{
|
||||
Net: "tcp",
|
||||
TsigSecret: map[string]string{dynUpdateKey: dynUpdateSecret},
|
||||
}
|
||||
resp, _, err := client.Exchange(update, tcp)
|
||||
if err != nil {
|
||||
t.Fatalf("sending signed UPDATE: %v", err)
|
||||
}
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("UPDATE rcode = %s, want NOERROR", dns.RcodeToString[resp.Rcode])
|
||||
}
|
||||
|
||||
transfer := &dns.Transfer{
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
}
|
||||
query := new(dns.Msg)
|
||||
query.SetAxfr("example.org.")
|
||||
envelopes, err := transfer.In(query, tcp)
|
||||
if err != nil {
|
||||
t.Fatalf("starting AXFR: %v", err)
|
||||
}
|
||||
var records []dns.RR
|
||||
for envelope := range envelopes {
|
||||
if envelope.Error != nil {
|
||||
t.Fatalf("AXFR envelope: %v", envelope.Error)
|
||||
}
|
||||
records = append(records, envelope.RR...)
|
||||
}
|
||||
if len(records) < 2 || records[0].Header().Rrtype != dns.TypeSOA || records[len(records)-1].Header().Rrtype != dns.TypeSOA {
|
||||
t.Fatalf("unexpected AXFR framing: %v", records)
|
||||
}
|
||||
found := false
|
||||
for _, transferred := range records {
|
||||
if transferred.String() == rr.String() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("AXFR did not include %s", rr)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
var contents = map[string]string{
|
||||
"Kexample.org.+013+45330.key": examplePub,
|
||||
"Kexample.org.+013+45330.private": examplePriv,
|
||||
"example.org.zone": exampleOrg,
|
||||
"example.org.signed": exampleOrg, // not signed, but does not matter for this test.
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user