diff --git a/.gitignore b/.gitignore index c440e43..ba11a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coredns tilt_modules .helm .vscode +.idea \ No newline at end of file diff --git a/apex.go b/apex.go index d18509c..a19532f 100644 --- a/apex.go +++ b/apex.go @@ -7,34 +7,6 @@ import ( "github.com/miekg/dns" ) -// serveApex serves request that hit the zone' apex. A reply is written back to the client. -func (gw *Gateway) serveApex(state request.Request) (int, error) { - m := new(dns.Msg) - m.SetReply(state.Req) - switch state.QType() { - case dns.TypeSOA: - // Force to true to fix broken behaviour of legacy glibc `getaddrinfo`. - // See https://github.com/coredns/coredns/pull/3573 - m.Authoritative = true - m.Answer = []dns.RR{gw.soa(state)} - case dns.TypeNS: - m.Answer = gw.nameservers(state) - - addr := gw.ExternalAddrFunc(state) - for _, rr := range addr { - rr.Header().Ttl = gw.ttlSOA - m.Extra = append(m.Extra, rr) - } - default: - m.Ns = []dns.RR{gw.soa(state)} - } - - if err := state.W.WriteMsg(m); err != nil { - log.Errorf("Failed to send a response: %s", err) - } - return 0, nil -} - // serveSubApex serves requests that hit the zones fake 'dns' subdomain where our nameservers live. func (gw *Gateway) serveSubApex(state request.Request) (int, error) { base, _ := dnsutil.TrimZone(state.Name(), state.Zone) diff --git a/apex_dual_test.go b/apex_dual_test.go index 95dd0ce..9e5d96d 100644 --- a/apex_dual_test.go +++ b/apex_dual_test.go @@ -2,6 +2,7 @@ package gateway import ( "context" + "net" "testing" "github.com/coredns/coredns/plugin/pkg/dnstest" @@ -11,6 +12,16 @@ import ( "github.com/miekg/dns" ) +func setupEmptyLookupFuncs() { + + if resource := lookupResource("Ingress"); resource != nil { + resource.lookup = func(_ []string) []net.IP { return []net.IP{} } + } + if resource := lookupResource("Service"); resource != nil { + resource.lookup = func(_ []string) []net.IP { return []net.IP{} } + } +} + func TestDualNS(t *testing.T) { ctrl := &KubeController{hasSynced: true} @@ -20,6 +31,7 @@ func TestDualNS(t *testing.T) { gw.Controller = ctrl gw.ExternalAddrFunc = selfDualAddressTest gw.secondNS = "dns2.kube-system" + setupEmptyLookupFuncs() ctx := context.TODO() for i, tc := range testsDualNS { @@ -49,7 +61,7 @@ var testsDualNS = []test.Case{ { Qname: "example.com.", Qtype: dns.TypeSOA, Rcode: dns.RcodeSuccess, - Answer: []dns.RR{ + Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, diff --git a/apex_test.go b/apex_test.go index bfb1e70..12eb641 100644 --- a/apex_test.go +++ b/apex_test.go @@ -19,6 +19,7 @@ func TestApex(t *testing.T) { gw.Next = test.NextHandler(dns.RcodeSuccess, nil) gw.Controller = ctrl gw.ExternalAddrFunc = selfAddressTest + setupEmptyLookupFuncs() ctx := context.TODO() for i, tc := range testsApex { @@ -48,7 +49,7 @@ var testsApex = []test.Case{ { Qname: "example.com.", Qtype: dns.TypeSOA, Rcode: dns.RcodeSuccess, - Answer: []dns.RR{ + Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, diff --git a/gateway.go b/gateway.go index ccde7de..ec355d8 100644 --- a/gateway.go +++ b/gateway.go @@ -118,10 +118,11 @@ func (gw *Gateway) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms return dns.RcodeServerFailure, plugin.Error(thisPlugin, fmt.Errorf("Could not sync required resources")) } + var isRootZoneQuery bool for _, z := range gw.Zones { if state.Name() == z { // apex query - ret, err := gw.serveApex(state) - return ret, err + isRootZoneQuery = true + break } if dns.IsSubDomain(gw.apex+"."+z, state.Name()) { // dns subdomain test for ns. and dns. queries @@ -142,39 +143,55 @@ func (gw *Gateway) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms } log.Debugf("Computed response addresses %v", addrs) + // Fall through if no host matches + if len(addrs) == 0 && gw.Fall.Through(qname) { + return plugin.NextOrFailure(gw.Name(), gw.Next, ctx, w, r) + } + m := new(dns.Msg) m.SetReply(state.Req) - // If there's no match, fall through or return NXDOMAIN - if len(addrs) == 0 { - if gw.Fall.Through(qname) { - return plugin.NextOrFailure(gw.Name(), gw.Next, ctx, w, r) - } + switch state.QType() { + case dns.TypeA: - m.Rcode = dns.RcodeNameError - m.Ns = []dns.RR{gw.soa(state)} - if err := w.WriteMsg(m); err != nil { - log.Errorf("Failed to send a response: %s", err) + if len(addrs) == 0 { + + if !isRootZoneQuery { + // No match, return NXDOMAIN + m.Rcode = dns.RcodeNameError + } + + m.Ns = []dns.RR{gw.soa(state)} + + } else { + + m.Answer = gw.A(state.Name(), addrs) + // Force to true to fix broken behaviour of legacy glibc `getaddrinfo`. + // See https://github.com/coredns/coredns/pull/3573 + m.Authoritative = true } - return 0, nil - } + case dns.TypeSOA: - switch state.QType() { - case dns.TypeA: - m.Answer = gw.A(state.Name(), addrs) // Force to true to fix broken behaviour of legacy glibc `getaddrinfo`. // See https://github.com/coredns/coredns/pull/3573 m.Authoritative = true - default: m.Ns = []dns.RR{gw.soa(state)} - } - // If there's no match, fall through or return the SOA - if len(m.Answer) == 0 { - if gw.Fall.Through(qname) { - return plugin.NextOrFailure(gw.Name(), gw.Next, ctx, w, r) + case dns.TypeNS: + + if isRootZoneQuery { + m.Answer = gw.nameservers(state) + + addr := gw.ExternalAddrFunc(state) + for _, rr := range addr { + rr.Header().Ttl = gw.ttlSOA + m.Extra = append(m.Extra, rr) + } + } else { + m.Ns = []dns.RR{gw.soa(state)} } + default: m.Ns = []dns.RR{gw.soa(state)} } diff --git a/gateway_test.go b/gateway_test.go index 7c360a0..5dd5527 100644 --- a/gateway_test.go +++ b/gateway_test.go @@ -2,16 +2,29 @@ package gateway import ( "context" + "errors" "net" "strings" "testing" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/test" "github.com/miekg/dns" ) +type FallthroughCase struct { + test.Case + FallthroughZones []string + FallthroughExpected bool +} + +type Fallen struct { + error +} + func TestLookup(t *testing.T) { real := []string{"Ingress", "Service"} fake := []string{"Gateway", "Pod"} @@ -64,78 +77,105 @@ func TestGateway(t *testing.T) { } } +func TestGatewayFallthrough(t *testing.T) { + + ctrl := &KubeController{hasSynced: true} + gw := newGateway() + gw.Zones = []string{"example.com."} + gw.Next = test.NextHandler(dns.RcodeSuccess, Fallen{}) + gw.ExternalAddrFunc = selfAddressTest + gw.Controller = ctrl + setupTestLookupFuncs() + + ctx := context.TODO() + for i, tc := range testsFallthrough { + r := tc.Msg() + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + gw.Fall = fall.F{Zones: tc.FallthroughZones} + _, err := gw.ServeDNS(ctx, w, r) + + if errors.As(err, &Fallen{}) && !tc.FallthroughExpected { + t.Fatalf("Test %d query resulted unexpectidly in a fall through instead of a response", i) + } + if err == nil && tc.FallthroughExpected { + t.Fatalf("Test %d query resulted unexpectidly in a response instead of a fall through", i) + } + } +} + var tests = []test.Case{ - // Existing Service + // Existing Service | Test 0 { Qname: "svc1.ns1.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ test.A("svc1.ns1.example.com. 60 IN A 192.0.1.1"), }, }, - // Existing Ingress + // Existing Ingress | Test 1 { Qname: "domain.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ test.A("domain.example.com. 60 IN A 192.0.0.1"), }, }, - // Ingress takes precedence over services + // Ingress takes precedence over services | Test 2 { Qname: "svc2.ns1.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ test.A("svc2.ns1.example.com. 60 IN A 192.0.0.2"), }, }, - // Non-existing Service + // Non-existing Service | Test 3 { Qname: "svcX.ns1.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError, Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, - // Non-existing Ingress + // Non-existing Ingress | Test 4 { Qname: "d0main.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError, Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, - // SOA for the existing domain + // SOA for the existing domain | Test 5 { Qname: "domain.example.com.", Qtype: dns.TypeSOA, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, - // Service with no public addresses + // Service with no public addresses | Test 6 { Qname: "svc3.ns1.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError, Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, - // Real service, wrong query type + // Real service, wrong query type | Test 7 { - Qname: "svc3.ns1.example.com.", Qtype: dns.TypeAAAA, Rcode: dns.RcodeNameError, + Qname: "svc3.ns1.example.com.", Qtype: dns.TypeCNAME, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, - // Ingress FQDN == zone + // Ingress FQDN == zone | Test 8 { Qname: "example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, - Ns: []dns.RR{ - test.SOA("example.com. 60 IN SOA dns1.kube-system.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + Answer: []dns.RR{ + test.A("example.com. 60 IN A 192.0.0.3"), }, }, - // Existing Ingress with a mix of lower and upper case letters + // Existing Ingress with a mix of lower and upper case letters | Test 9 { Qname: "dOmAiN.eXamPLe.cOm.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ test.A("domain.example.com. 60 IN A 192.0.0.1"), }, }, - // Existing Service with a mix of lower and upper case letters + // Existing Service with a mix of lower and upper case letters | Test 10 { Qname: "svC1.Ns1.exAmplE.Com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ @@ -144,6 +184,34 @@ var tests = []test.Case{ }, } +var testsFallthrough = []FallthroughCase{ + // Match found, fallthrough enabled | Test 0 + { + Case: test.Case{Qname: "example.com.", Qtype: dns.TypeA}, + FallthroughZones: []string{"."}, FallthroughExpected: false, + }, + // No match found, fallthrough enabled | Test 1 + { + Case: test.Case{Qname: "non-existent.example.com.", Qtype: dns.TypeA}, + FallthroughZones: []string{"."}, FallthroughExpected: true, + }, + // Match found, fallthrough for different zone | Test 2 + { + Case: test.Case{Qname: "example.com.", Qtype: dns.TypeA}, + FallthroughZones: []string{"not-example.com."}, FallthroughExpected: false, + }, + // No match found, fallthrough for different zone | Test 3 + { + Case: test.Case{Qname: "non-existent.example.com.", Qtype: dns.TypeA}, + FallthroughZones: []string{"not-example.com."}, FallthroughExpected: false, + }, + // No fallthrough on gw apex | Test 4 + { + Case: test.Case{Qname: "dns1.kube-system.example.com.", Qtype: dns.TypeA}, + FallthroughZones: []string{"."}, FallthroughExpected: false, + }, +} + var testServiceIndexes = map[string][]net.IP{ "svc1.ns1": {net.ParseIP("192.0.1.1")}, "svc2.ns1": {net.ParseIP("192.0.1.2")},