mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 12:05:22 -04:00
* 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>
380 lines
12 KiB
Go
380 lines
12 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|