diff --git a/pkg/issuer/acme/dns/rfc2136/BUILD.bazel b/pkg/issuer/acme/dns/rfc2136/BUILD.bazel index ccd8c2e69..287c4bf63 100644 --- a/pkg/issuer/acme/dns/rfc2136/BUILD.bazel +++ b/pkg/issuer/acme/dns/rfc2136/BUILD.bazel @@ -31,10 +31,11 @@ go_test( embed = [":go_default_library"], deps = [ "//pkg/apis/certmanager/v1alpha1:go_default_library", + "//pkg/logs:go_default_library", "//test/acme/dns:go_default_library", + "//test/acme/dns/server:go_default_library", "//vendor/github.com/miekg/dns:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", - "//vendor/k8s.io/klog:go_default_library", ], ) diff --git a/pkg/issuer/acme/dns/rfc2136/provider_test.go b/pkg/issuer/acme/dns/rfc2136/provider_test.go index 382e22c15..f6328bc48 100644 --- a/pkg/issuer/acme/dns/rfc2136/provider_test.go +++ b/pkg/issuer/acme/dns/rfc2136/provider_test.go @@ -19,25 +19,28 @@ package rfc2136 import ( "testing" - dnslib "github.com/miekg/dns" - cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + logf "github.com/jetstack/cert-manager/pkg/logs" "github.com/jetstack/cert-manager/test/acme/dns" + testserver "github.com/jetstack/cert-manager/test/acme/dns/server" ) func TestRunSuiteWithTSIG(t *testing.T) { - dnslib.HandleFunc(rfc2136TestZone, (&basicStatefulServer{}).serverHandlerPassBackRequest) - defer dnslib.HandleRemove(rfc2136TestZone) - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", true) - if err != nil { - t.Errorf("error starting test dns server: %v", err) - t.FailNow() + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + EnableTSIG: true, + TSIGZone: rfc2136TestZone, + TSIGKeyName: rfc2136TestTsigKeyName, + TSIGKeySecret: rfc2136TestTsigSecret, + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() var validConfig = cmapi.ACMEIssuerDNS01ProviderRFC2136{ - Nameserver: addrstr, + Nameserver: server.ListenAddr(), TSIGSecret: cmapi.SecretKeySelector{ LocalObjectReference: cmapi.LocalObjectReference{ Name: "testkey", @@ -52,7 +55,7 @@ func TestRunSuiteWithTSIG(t *testing.T) { dns.SetResolvedFQDN(rfc2136TestFqdn), dns.SetAllowAmbientCredentials(false), dns.SetConfig(validConfig), - dns.SetDNSServer(addrstr), + dns.SetDNSServer(server.ListenAddr()), dns.SetManifestPath("testdata"), // Disable recursive NS lookups as we run a single authoritative NS per test dns.SetUseAuthoritative(false), @@ -62,18 +65,17 @@ func TestRunSuiteWithTSIG(t *testing.T) { } func TestRunSuiteNoTSIG(t *testing.T) { - dnslib.HandleFunc(rfc2136TestZone, (&basicStatefulServer{}).serverHandlerPassBackRequest) - defer dnslib.HandleRemove(rfc2136TestZone) - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) - if err != nil { - t.Errorf("error starting test dns server: %v", err) - t.FailNow() + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() var validConfig = cmapi.ACMEIssuerDNS01ProviderRFC2136{ - Nameserver: addrstr, + Nameserver: server.ListenAddr(), } fixture := dns.NewFixture(&Solver{}, @@ -81,7 +83,7 @@ func TestRunSuiteNoTSIG(t *testing.T) { dns.SetResolvedFQDN(rfc2136TestFqdn), dns.SetAllowAmbientCredentials(false), dns.SetConfig(validConfig), - dns.SetDNSServer(addrstr), + dns.SetDNSServer(server.ListenAddr()), dns.SetManifestPath("testdata"), // Disable recursive NS lookups as we run a single authoritative NS per test dns.SetUseAuthoritative(false), diff --git a/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go b/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go index c009e38eb..da0ab019c 100644 --- a/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go +++ b/pkg/issuer/acme/dns/rfc2136/rfc2136_test.go @@ -23,15 +23,15 @@ package rfc2136 import ( "fmt" - "net" "strings" - "sync" "testing" "time" "github.com/miekg/dns" "github.com/stretchr/testify/assert" - "k8s.io/klog" + + logf "github.com/jetstack/cert-manager/pkg/logs" + testserver "github.com/jetstack/cert-manager/test/acme/dns/server" ) var ( @@ -46,19 +46,20 @@ var ( ) func TestRFC2136CanaryLocalTestServer(t *testing.T) { - dns.HandleFunc("example.com.", serverHandlerHello) - defer dns.HandleRemove("example.com.") - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) - if err != nil { - t.Fatalf("Failed to start test server: %v", err) + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + Handler: dns.HandlerFunc(serverHandlerHello), + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() c := new(dns.Client) m := new(dns.Msg) m.SetQuestion("example.com.", dns.TypeTXT) - r, _, err := c.Exchange(m, addrstr) + r, _, err := c.Exchange(m, server.ListenAddr()) if err != nil || len(r.Extra) == 0 { t.Fatalf("Failed to communicate with test server: %v", err) } @@ -69,17 +70,17 @@ func TestRFC2136CanaryLocalTestServer(t *testing.T) { } func TestRFC2136ServerSuccess(t *testing.T) { - dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) - defer dns.HandleRemove(rfc2136TestZone) - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) - - if err != nil { - t.Fatalf("Failed to start test server: %v", err) + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + Handler: dns.HandlerFunc(serverHandlerReturnSuccess), + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() - provider, err := NewDNSProviderCredentials(addrstr, "", "", "") + provider, err := NewDNSProviderCredentials(server.ListenAddr(), "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) } @@ -89,16 +90,17 @@ func TestRFC2136ServerSuccess(t *testing.T) { } func TestRFC2136ServerError(t *testing.T) { - dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr) - defer dns.HandleRemove(rfc2136TestZone) - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) - if err != nil { - t.Fatalf("Failed to start test server: %v", err) + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + Handler: dns.HandlerFunc(serverHandlerReturnErr), + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() - provider, err := NewDNSProviderCredentials(addrstr, "", "", "") + provider, err := NewDNSProviderCredentials(server.ListenAddr(), "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) } @@ -110,16 +112,21 @@ func TestRFC2136ServerError(t *testing.T) { } func TestRFC2136TsigClient(t *testing.T) { - dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) - defer dns.HandleRemove(rfc2136TestZone) - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", true) - if err != nil { - t.Fatalf("Failed to start test server: %v", err) + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + Handler: dns.HandlerFunc(serverHandlerReturnSuccess), + EnableTSIG: true, + TSIGZone: rfc2136TestZone, + TSIGKeyName: rfc2136TestTsigKeyName, + TSIGKeySecret: rfc2136TestTsigSecret, + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() - provider, err := NewDNSProviderCredentials(addrstr, "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) + provider, err := NewDNSProviderCredentials(server.ListenAddr(), "", rfc2136TestTsigKeyName, rfc2136TestTsigSecret) if err != nil { t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) } @@ -206,12 +213,12 @@ func TestRFC2136InvalidTSIGAlgorithm(t *testing.T) { } func TestRFC2136ValidUpdatePacket(t *testing.T) { - dns.HandleFunc(rfc2136TestZone, (&basicStatefulServer{}).serverHandlerPassBackRequest) - defer dns.HandleRemove(rfc2136TestZone) - - server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false) - if err != nil { - t.Fatalf("Failed to start test server: %v", err) + ctx := logf.NewContext(nil, nil, t.Name()) + server := &testserver.BasicServer{ + Zones: []string{rfc2136TestZone}, + } + if err := server.Run(ctx); err != nil { + t.Fatalf("failed to start test server: %v", err) } defer server.Shutdown() @@ -221,13 +228,8 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) { m.SetUpdate(rfc2136TestZone) m.RemoveRRset(rrs) m.Insert(rrs) - //expectstr := m.String() - //expect, err := m.Pack() - if err != nil { - t.Fatalf("Error packing expect msg: %v", err) - } - provider, err := NewDNSProviderCredentials(addrstr, "", "", "") + provider, err := NewDNSProviderCredentials(server.ListenAddr(), "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err) } @@ -239,29 +241,6 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) { assert.NoError(t, err) } -func runLocalDNSTestServer(listenAddr string, tsig bool) (*dns.Server, string, error) { - pc, err := net.ListenPacket("udp", listenAddr) - if err != nil { - return nil, "", err - } - server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour} - if tsig { - server.TsigSecret = map[string]string{rfc2136TestTsigKeyName: rfc2136TestTsigSecret} - } - - waitLock := sync.Mutex{} - waitLock.Lock() - server.NotifyStartedFunc = waitLock.Unlock - - go func() { - server.ActivateAndServe() - pc.Close() - }() - - waitLock.Lock() - return server, pc.LocalAddr().String(), nil -} - func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) @@ -297,56 +276,3 @@ func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) { m.SetRcode(req, dns.RcodeNotZone) w.WriteMsg(m) } - -type basicStatefulServer struct { - txtRecords map[string][]string -} - -func (b *basicStatefulServer) serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) { - if b.txtRecords == nil { - b.txtRecords = make(map[string][]string) - } - - m := new(dns.Msg) - m.SetReply(req) - defer w.WriteMsg(m) - - if t := req.IsTsig(); t != nil { - if w.TsigStatus() == nil { - // Validated - m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix()) - } - } - - if (req.Opcode != dns.OpcodeUpdate && req.Opcode != dns.OpcodeQuery) || req.Question[0].Qclass != dns.ClassINET { - klog.Infof("skipping dns packet: %#v", req) - //m.Rcode = dns.RcodeServerFailure - return - } - - if req.Opcode == dns.OpcodeUpdate { - for _, rr := range req.Ns { - txt := rr.(*dns.TXT) - if rr.Header().Class == dns.ClassNONE { - klog.Infof("deleting val %q", txt.Hdr.Name) - delete(b.txtRecords, txt.Hdr.Name) - continue - } - klog.Infof("setting value %q: %v", txt.Hdr.Name, txt.Txt) - b.txtRecords[txt.Hdr.Name] = txt.Txt - } - } - - switch req.Question[0].Qtype { - case dns.TypeSOA: - // Return SOA to appease findZoneByFqdn() - soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone)) - m.Answer = []dns.RR{soaRR} - case dns.TypeTXT: - for _, rr := range b.txtRecords[req.Question[0].Name] { - klog.Infof("returning %q", fmt.Sprintf("%s %d IN TXT %s", req.Question[0].Name, rfc2136TestTTL, rr)) - txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", req.Question[0].Name, rfc2136TestTTL, rr)) - m.Answer = append(m.Answer, txtRR) - } - } -} diff --git a/test/acme/dns/BUILD.bazel b/test/acme/dns/BUILD.bazel index ebf501b6d..595dda338 100644 --- a/test/acme/dns/BUILD.bazel +++ b/test/acme/dns/BUILD.bazel @@ -39,7 +39,10 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//test/acme/dns/server:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/test/acme/dns/server/BUILD.bazel b/test/acme/dns/server/BUILD.bazel new file mode 100644 index 000000000..17d1bb679 --- /dev/null +++ b/test/acme/dns/server/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "rfc2136.go", + "server.go", + ], + importpath = "github.com/jetstack/cert-manager/test/acme/dns/server", + visibility = ["//visibility:public"], + deps = [ + "//pkg/logs:go_default_library", + "//vendor/github.com/go-logr/logr:go_default_library", + "//vendor/github.com/miekg/dns:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/test/acme/dns/server/doc.go b/test/acme/dns/server/doc.go new file mode 100644 index 000000000..a9a8abfa0 --- /dev/null +++ b/test/acme/dns/server/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package server implements an extremely basic DNS server that only responds +// to a very limited subset of DNS requests. +// It is suitable for use during testing RFC2136 updates and TXT record lookup. + +package server diff --git a/test/acme/dns/server/rfc2136.go b/test/acme/dns/server/rfc2136.go new file mode 100644 index 000000000..47d9bd3f4 --- /dev/null +++ b/test/acme/dns/server/rfc2136.go @@ -0,0 +1,109 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "fmt" + "sync" + "time" + + "github.com/go-logr/logr" + "github.com/miekg/dns" +) + +type rfc2136Handler struct { + log logr.Logger + + txtRecords map[string][]string + zones []string + tsigZone string + lock sync.Mutex +} + +// serveDNS implements github.com/miekg/dns.Handler +func (b *rfc2136Handler) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { + b.lock.Lock() + defer b.lock.Unlock() + log := b.log.WithName("serveDNS") + + m := new(dns.Msg) + m.SetReply(req) + defer w.WriteMsg(m) + + var zone string + if len(req.Question) > 0 { + question := req.Question[0].Name + log = log.WithValues("question", question, "opcode", dns.OpcodeToString[req.Opcode]) + zone = b.zoneForFQDN(question) + if zone == "" { + log.Info("failed to lookup zone for fqdn") + m.Rcode = dns.RcodeServerFailure + return + } + log = log.WithValues("zone", zone) + } + + if t := req.IsTsig(); t != nil { + log.Info("TSIG requested on DNS request") + if w.TsigStatus() == nil { + log.Info("setting TSIG values on response") + // Validated + m.SetTsig(b.tsigZone, dns.HmacMD5, 300, time.Now().Unix()) + } + } + + // updates are currently accepted for *all* zones + if req.Opcode == dns.OpcodeUpdate { + for _, rr := range req.Ns { + txt := rr.(*dns.TXT) + log := log.WithValues("value", txt.Hdr.Name, "class", dns.ClassToString[rr.Header().Class], "txt", txt.Txt) + if rr.Header().Class == dns.ClassNONE { + log.Info("deleting txt record value due to NONE class") + // TODO: can we only delete the named record here somehow? + delete(b.txtRecords, txt.Hdr.Name) + continue + } + log.Info("setting TXT record value") + b.txtRecords[txt.Hdr.Name] = txt.Txt + } + } + + switch req.Question[0].Qtype { + case dns.TypeSOA: + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", zone, defaultTTL, zone, zone)) + m.Answer = []dns.RR{soaRR} + case dns.TypeTXT: + for _, rr := range b.txtRecords[req.Question[0].Name] { + txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", req.Question[0].Name, defaultTTL, rr)) + m.Answer = append(m.Answer, txtRR) + } + } + + for _, rr := range m.Answer { + log.Info("responding", "response", rr.String()) + } +} + +func (b *rfc2136Handler) zoneForFQDN(s string) string { + for _, z := range b.zones { + if dns.IsSubDomain(z, s) { + return z + } + } + return "" +} diff --git a/test/acme/dns/server/server.go b/test/acme/dns/server/server.go new file mode 100644 index 000000000..a41d0c81c --- /dev/null +++ b/test/acme/dns/server/server.go @@ -0,0 +1,121 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/miekg/dns" + + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +const ( + defaultTTL = 60 +) + +type BasicServer struct { + // Zones is a list of DNS zones that this server should accept responses + // for. + Zones []string + + // Handler is an optional + Handler dns.Handler + + // TSIG configuration options + // EnableTSIG enables TSIG support for the DNS server + // If true, both TSIGKeyName and TSIGKeySecret must be provided. + EnableTSIG bool + // TSIGKeyName to be used in responses when TSIG is enabled + TSIGKeyName string + // TSIGKeySecret to be used in responses when TSIG is enabled + TSIGKeySecret string + // TSIGZone is the DNS zone that should be used in TSIG responses + TSIGZone string + + ctx context.Context + listenAddr string + server *dns.Server +} + +// Run starts the test DNS server, binding to a random port on 127.0.0.1 +func (b *BasicServer) Run(ctx context.Context) error { + return b.RunWithAddress(ctx, "127.0.0.1:0") +} + +// RunWithAddress starts the test DNS server using the specified listen address. +func (b *BasicServer) RunWithAddress(ctx context.Context, listenAddr string) error { + log := logf.FromContext(ctx, "dnsBasicServer") + + if listenAddr == "" { + return fmt.Errorf("listen address must be provided") + } + + pc, err := net.ListenPacket("udp", listenAddr) + if err != nil { + return err + } + b.listenAddr = pc.LocalAddr().String() + log = log.WithValues("address", b.listenAddr) + log.Info("listening on UDP port") + + // update the ctx with the new logger + ctx = logf.NewContext(ctx, log) + + b.server = &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour} + if b.EnableTSIG { + log.Info("enabling TSIG support") + b.server.TsigSecret = map[string]string{b.TSIGKeyName: b.TSIGKeySecret} + } + + if b.Handler == nil { + b.Handler = &rfc2136Handler{ + log: log, + txtRecords: make(map[string][]string), + zones: b.Zones, + tsigZone: b.TSIGZone, + } + } + b.server.Handler = b.Handler + + // Start the DNS server in a separate goroutine and wait for it to start + waitLock := sync.Mutex{} + waitLock.Lock() + b.server.NotifyStartedFunc = waitLock.Unlock + go func() { + log.Info("starting DNS server") + b.server.ActivateAndServe() + log.Info("DNS server exited") + pc.Close() + }() + waitLock.Lock() + defer waitLock.Unlock() + + return nil +} + +func (b *BasicServer) ListenAddr() string { + return b.listenAddr +} + +func (b *BasicServer) Shutdown() error { + return b.server.Shutdown() +}