// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package agent /* Note: this file got to be 10k lines long and caused multiple IDE issues * as well as GitHub's UI unable to display diffs with large changes to this file. * This file has been broken up by moving: * - Node Lookup tests into dns_node_lookup_test.go * - Service Lookup tests into dn_service_lookup_test.go * * Please be aware of the size of each of these files and add tests / break * up tests accordingly. */ import ( "context" "errors" "fmt" "math" "math/rand" "net" "reflect" "strings" "testing" "time" "github.com/miekg/dns" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" "github.com/hashicorp/serf/coordinate" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/discovery" dnsConsul "github.com/hashicorp/consul/agent/dns" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/internal/gossip/librtt" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" ) const ( configUDPAnswerLimit = 4 defaultNumUDPResponses = 3 testUDPTruncateLimit = 8 pctNodesWithIPv6 = 0.5 // generateNumNodes is the upper bounds for the number of hosts used // in testing below. Generate an arbitrarily large number of hosts. generateNumNodes = testUDPTruncateLimit * defaultNumUDPResponses * configUDPAnswerLimit ) // makeRecursor creates a generic DNS server which always returns // the provided reply. This is useful for mocking a DNS recursor with // an expected result. func makeRecursor(t *testing.T, answer dns.Msg) *dns.Server { a := answer mux := dns.NewServeMux() mux.HandleFunc(".", func(resp dns.ResponseWriter, msg *dns.Msg) { // The SetReply function sets the return code of the DNS // query to SUCCESS // We need a way to copy the variables not addressed // in SetReply answer.SetReply(msg) answer.Rcode = a.Rcode if err := resp.WriteMsg(&answer); err != nil { t.Fatalf("err: %s", err) } }) up := make(chan struct{}) server := &dns.Server{ Addr: "127.0.0.1:0", Net: "udp", Handler: mux, NotifyStartedFunc: func() { close(up) }, } go server.ListenAndServe() <-up server.Addr = server.PacketConn.LocalAddr().String() return server } // dnsCNAME returns a DNS CNAME record struct func dnsCNAME(src, dest string) *dns.CNAME { return &dns.CNAME{ Hdr: dns.RR_Header{ Name: dns.Fqdn(src), Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: dns.Fqdn(dest), } } // dnsA returns a DNS A record struct func dnsA(src, dest string) *dns.A { return &dns.A{ Hdr: dns.RR_Header{ Name: dns.Fqdn(src), Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(dest), } } // dnsTXT returns a DNS TXT record struct func dnsTXT(src string, txt []string) *dns.TXT { return &dns.TXT{ Hdr: dns.RR_Header{ Name: dns.Fqdn(src), Rrtype: dns.TypeTXT, Class: dns.ClassINET, }, Txt: txt, } } func getVersionHCL(enableV2 bool) map[string]string { versions := map[string]string{ "DNS: v1 / Catalog: v1": "experiments=[\"v1dns\"]", } if enableV2 { versions["DNS: v2 / Catalog: v1"] = "" } return versions } // Copied to agent/dns/recursor_test.go func TestDNS_RecursorAddr(t *testing.T) { addr, err := recursorAddr("8.8.8.8") if err != nil { t.Fatalf("err: %v", err) } if addr != "8.8.8.8:53" { t.Fatalf("bad: %v", addr) } addr, err = recursorAddr("2001:4860:4860::8888") if err != nil { t.Fatalf("err: %v", err) } if addr != "[2001:4860:4860::8888]:53" { t.Fatalf("bad: %v", addr) } _, err = recursorAddr("1.2.3.4::53") if err == nil || !strings.Contains(err.Error(), "too many colons in address") { t.Fatalf("err: %v", err) } _, err = recursorAddr("2001:4860:4860::8888:::53") if err == nil || !strings.Contains(err.Error(), "too many colons in address") { t.Fatalf("err: %v", err) } } func TestDNS_EncodeKVasRFC1464(t *testing.T) { // Test cases are from rfc1464 type rfc1464Test struct { key, value, internalForm, externalForm string } tests := []rfc1464Test{ {"color", "blue", "color=blue", "color=blue"}, {"equation", "a=4", "equation=a=4", "equation=a=4"}, {"a=a", "true", "a`=a=true", "a`=a=true"}, {"a\\=a", "false", "a\\`=a=false", "a\\`=a=false"}, {"=", "\\=", "`==\\=", "`==\\="}, {"string", "\"Cat\"", "string=\"Cat\"", "string=\"Cat\""}, {"string2", "`abc`", "string2=``abc``", "string2=``abc``"}, {"novalue", "", "novalue=", "novalue="}, {"a b", "c d", "a b=c d", "a b=c d"}, {"abc ", "123 ", "abc` =123 ", "abc` =123 "}, // Additional tests {" abc", " 321", "` abc= 321", "` abc= 321"}, {"`a", "b", "``a=b", "``a=b"}, } for _, test := range tests { answer := encodeKVasRFC1464(test.key, test.value) require.Equal(t, test.internalForm, answer) } } func TestDNS_Over_TCP(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register node args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "Foo", Address: "127.0.0.1", } var out struct{} if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } m := new(dns.Msg) m.SetQuestion("foo.node.dc1.consul.", dns.TypeANY) c := new(dns.Client) c.Net = "tcp" in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 1 { t.Fatalf("empty lookup: %#v", in) } }) } } func TestDNS_EmptyAltDomain(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("consul.service.", dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) require.NoError(t, err) require.Empty(t, in.Answer) }) } } func TestDNS_CycleRecursorCheck(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } // Start a DNS recursor that returns a SERVFAIL server1 := makeRecursor(t, dns.Msg{ MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure}, }) // Start a DNS recursor that returns the result defer server1.Shutdown() server2 := makeRecursor(t, dns.Msg{ MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess}, Answer: []dns.RR{ dnsA("www.google.com", "172.21.45.67"), }, }) defer server2.Shutdown() for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { // Mock the agent startup with the necessary configs agent := NewTestAgent(t, `recursors = ["`+server1.Addr+`", "`+server2.Addr+`"] `+experimentsHCL) defer agent.Shutdown() // DNS Message init m := new(dns.Msg) m.SetQuestion("google.com.", dns.TypeA) // Agent request client := new(dns.Client) in, _, _ := client.Exchange(m, agent.DNSAddr()) wantAnswer := []dns.RR{ &dns.A{ Hdr: dns.RR_Header{Name: "www.google.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4}, A: []byte{0xAC, 0x15, 0x2D, 0x43}, // 172 , 21, 45, 67 }, } require.NotNil(t, in) require.Equal(t, wantAnswer, in.Answer) }) } } func TestDNS_CycleRecursorCheckAllFail(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } // Start 3 DNS recursors that returns a REFUSED status server1 := makeRecursor(t, dns.Msg{ MsgHdr: dns.MsgHdr{Rcode: dns.RcodeRefused}, }) defer server1.Shutdown() server2 := makeRecursor(t, dns.Msg{ MsgHdr: dns.MsgHdr{Rcode: dns.RcodeRefused}, }) defer server2.Shutdown() server3 := makeRecursor(t, dns.Msg{ MsgHdr: dns.MsgHdr{Rcode: dns.RcodeRefused}, }) defer server3.Shutdown() for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { // Mock the agent startup with the necessary configs agent := NewTestAgent(t, `recursors = ["`+server1.Addr+`", "`+server2.Addr+`","`+server3.Addr+`"] `+experimentsHCL) defer agent.Shutdown() // DNS dummy message initialization m := new(dns.Msg) m.SetQuestion("google.com.", dns.TypeA) // Agent request client := new(dns.Client) in, _, err := client.Exchange(m, agent.DNSAddr()) require.NoError(t, err) // Verify if we hit SERVFAIL from Consul require.NotNil(t, in) require.Equal(t, dns.RcodeServerFailure, in.Rcode) }) } } func TestDNS_EDNS0(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register node args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "foo", Address: "127.0.0.2", } var out struct{} if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } m := new(dns.Msg) m.SetEdns0(12345, true) m.SetQuestion("foo.node.dc1.consul.", dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 1 { t.Fatalf("empty lookup: %#v", in) } edns := in.IsEdns0() if edns == nil { t.Fatalf("empty edns: %#v", in) } if edns.UDPSize() != 12345 { t.Fatalf("bad edns size: %d", edns.UDPSize()) } }) } } func TestDNS_EDNS0_ECS(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register a node with a service. { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "foo", Address: "127.0.0.1", Service: &structs.NodeService{ Service: "db", Tags: []string{"primary"}, Port: 12345, }, } var out struct{} require.NoError(t, a.RPC(context.Background(), "Catalog.Register", args, &out)) } // Register an equivalent prepared query. var id string { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "test", Service: structs.ServiceQuery{ Service: "db", }, }, } require.NoError(t, a.RPC(context.Background(), "PreparedQuery.Apply", args, &id)) } cases := []struct { Name string Question string SubnetAddr string SourceNetmask uint8 ExpectedScope uint8 }{ {"global", "db.service.consul.", "198.18.0.1", 32, 0}, {"query", "test.query.consul.", "198.18.0.1", 32, 32}, {"query-subnet", "test.query.consul.", "198.18.0.0", 21, 21}, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { c := new(dns.Client) // Query the service directly - should have a globally valid scope (0) m := new(dns.Msg) edns := new(dns.OPT) edns.Hdr.Name = "." edns.Hdr.Rrtype = dns.TypeOPT edns.SetUDPSize(12345) edns.SetDo(true) subnetOp := new(dns.EDNS0_SUBNET) subnetOp.Code = dns.EDNS0SUBNET subnetOp.Family = 1 subnetOp.SourceNetmask = tc.SourceNetmask subnetOp.Address = net.ParseIP(tc.SubnetAddr) edns.Option = append(edns.Option, subnetOp) m.Extra = append(m.Extra, edns) m.SetQuestion(tc.Question, dns.TypeA) in, _, err := c.Exchange(m, a.DNSAddr()) require.NoError(t, err) require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok) require.Equal(t, "127.0.0.1", aRec.A.String()) optRR := in.IsEdns0() require.NotNil(t, optRR) require.Len(t, optRR.Option, 1) subnet, ok := optRR.Option[0].(*dns.EDNS0_SUBNET) require.True(t, ok) require.Equal(t, uint16(1), subnet.Family) require.Equal(t, tc.SourceNetmask, subnet.SourceNetmask) require.Equal(t, tc.ExpectedScope, subnet.SourceScope) require.Equal(t, net.ParseIP(tc.SubnetAddr), subnet.Address) }) } }) } } func TestDNS_SOA_Settings(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } testSoaWithConfig := func(config string, ttl, expire, refresh, retry uint) { a := NewTestAgent(t, config) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // lookup a non-existing node, we should receive a SOA m := new(dns.Msg) m.SetQuestion("nofoo.node.dc1.consul.", dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) require.NoError(t, err) require.Len(t, in.Ns, 1) soaRec, ok := in.Ns[0].(*dns.SOA) require.True(t, ok, "NS RR is not a SOA record") require.Equal(t, uint32(ttl), soaRec.Minttl) require.Equal(t, uint32(expire), soaRec.Expire) require.Equal(t, uint32(refresh), soaRec.Refresh) require.Equal(t, uint32(retry), soaRec.Retry) require.Equal(t, uint32(ttl), soaRec.Hdr.Ttl) } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { // Default configuration testSoaWithConfig(experimentsHCL, 0, 86400, 3600, 600) // Override all settings testSoaWithConfig("dns_config={soa={min_ttl=60,expire=43200,refresh=1800,retry=300}} "+experimentsHCL, 60, 43200, 1800, 300) // Override partial settings testSoaWithConfig("dns_config={soa={min_ttl=60,expire=43200}} "+experimentsHCL, 60, 43200, 3600, 600) // Override partial settings, part II testSoaWithConfig("dns_config={soa={refresh=1800,retry=300}} "+experimentsHCL, 0, 86400, 1800, 300) }) } } func TestDNS_VirtualIPLookup(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := StartTestAgent(t, TestAgent{HCL: experimentsHCL, Overrides: `peering = { test_allow_peer_registrations = true } log_level = "debug"`}) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") server, ok := a.delegate.(*consul.Server) require.True(t, ok) // The proxy service will not receive a virtual IP if the server is not assigning virtual IPs yet. retry.Run(t, func(r *retry.R) { _, entry, err := server.FSM().State().SystemMetadataGet(nil, structs.SystemMetadataVirtualIPsEnabled) require.NoError(r, err) require.NotNil(r, entry) }) type testCase struct { name string reg *structs.RegisterRequest question string expect string } run := func(t *testing.T, tc testCase) { var out struct{} require.Nil(t, a.RPC(context.Background(), "Catalog.Register", tc.reg, &out)) m := new(dns.Msg) m.SetQuestion(tc.question, dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) require.Nil(t, err) require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok) require.Equal(t, tc.expect, aRec.A.String()) } tt := []testCase{ { name: "local query", reg: &structs.RegisterRequest{ Datacenter: "dc1", Node: "foo", Address: "127.0.0.55", Service: &structs.NodeService{ Kind: structs.ServiceKindConnectProxy, Service: "web-proxy", Port: 12345, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "db", }, }, }, question: "db.virtual.consul.", expect: "240.0.0.1", }, { name: "query for imported service", reg: &structs.RegisterRequest{ PeerName: "frontend", Datacenter: "dc1", Node: "foo", Address: "127.0.0.55", Service: &structs.NodeService{ PeerName: "frontend", Kind: structs.ServiceKindConnectProxy, Service: "web-proxy", Port: 12345, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "db", }, }, }, question: "db.virtual.frontend.consul.", expect: "240.0.0.2", }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } }) } } func TestDNS_InifiniteRecursion(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } // This test should not create an infinite recursion for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` domain = "CONSUL." node_name = "test node" `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register the initial node with a service { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "web", Address: "web.service.consul.", Service: &structs.NodeService{ Service: "web", Port: 12345, Address: "web.service.consul.", }, } var out struct{} if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } } // Look up the service directly questions := []string{ "web.service.consul.", } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question, dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) < 1 { t.Fatalf("Bad: %#v", in) } aRec, ok := in.Answer[0].(*dns.CNAME) if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } if aRec.Target != "web.service.consul." { t.Fatalf("Bad: %#v, target:=%s", aRec, aRec.Target) } } }) } } func TestDNS_NSRecords(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` domain = "CONSUL." node_name = "server1" `+experimentsHCL) defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("something.node.consul.", dns.TypeNS) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } wantAnswer := []dns.RR{ &dns.NS{ Hdr: dns.RR_Header{Name: "consul.", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 0, Rdlength: 0x13}, Ns: "server1.node.dc1.consul.", }, } require.Equal(t, wantAnswer, in.Answer, "answer") wantExtra := []dns.RR{ &dns.A{ Hdr: dns.RR_Header{Name: "server1.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4, Ttl: 0}, A: net.ParseIP("127.0.0.1").To4(), }, } require.Equal(t, wantExtra, in.Extra, "extra") }) } } func TestDNS_AltDomain_NSRecords(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` domain = "CONSUL." node_name = "server1" alt_domain = "test-domain." `+experimentsHCL) defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") questions := []struct { ask string domain string wantDomain string }{ {"something.node.consul.", "consul.", "server1.node.dc1.consul."}, {"something.node.test-domain.", "test-domain.", "server1.node.dc1.test-domain."}, } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question.ask, dns.TypeNS) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } wantAnswer := []dns.RR{ &dns.NS{ Hdr: dns.RR_Header{Name: question.domain, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 0, Rdlength: 0x13}, Ns: question.wantDomain, }, } require.Equal(t, wantAnswer, in.Answer, "answer") wantExtra := []dns.RR{ &dns.A{ Hdr: dns.RR_Header{Name: question.wantDomain, Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4, Ttl: 0}, A: net.ParseIP("127.0.0.1").To4(), }, } require.Equal(t, wantExtra, in.Extra, "extra") } }) } } func TestDNS_NSRecords_IPV6(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` domain = "CONSUL." node_name = "server1" advertise_addr = "::1" `+experimentsHCL) defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("server1.node.dc1.consul.", dns.TypeNS) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } wantAnswer := []dns.RR{ &dns.NS{ Hdr: dns.RR_Header{Name: "consul.", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 0, Rdlength: 0x2}, Ns: "server1.node.dc1.consul.", }, } require.Equal(t, wantAnswer, in.Answer, "answer") wantExtra := []dns.RR{ &dns.AAAA{ Hdr: dns.RR_Header{Name: "server1.node.dc1.consul.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Rdlength: 0x10, Ttl: 0}, AAAA: net.ParseIP("::1"), }, } require.Equal(t, wantExtra, in.Extra, "extra") }) } } func TestDNS_AltDomain_NSRecords_IPV6(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` domain = "CONSUL." node_name = "server1" advertise_addr = "::1" alt_domain = "test-domain." `+experimentsHCL) defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") questions := []struct { ask string domain string wantDomain string }{ {"server1.node.dc1.consul.", "consul.", "server1.node.dc1.consul."}, {"server1.node.dc1.test-domain.", "test-domain.", "server1.node.dc1.test-domain."}, } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question.ask, dns.TypeNS) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } wantAnswer := []dns.RR{ &dns.NS{ Hdr: dns.RR_Header{Name: question.domain, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 0, Rdlength: 0x2}, Ns: question.wantDomain, }, } require.Equal(t, wantAnswer, in.Answer, "answer") wantExtra := []dns.RR{ &dns.AAAA{ Hdr: dns.RR_Header{Name: question.wantDomain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Rdlength: 0x10, Ttl: 0}, AAAA: net.ParseIP("::1"), }, } require.Equal(t, wantExtra, in.Extra, "extra") } }) } } func TestDNS_Lookup_TaggedIPAddresses(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register an equivalent prepared query. var id string { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "test", Service: structs.ServiceQuery{ Service: "db", }, }, } require.NoError(t, a.RPC(context.Background(), "PreparedQuery.Apply", args, &id)) } type testCase struct { nodeAddress string nodeTaggedAddresses map[string]string serviceAddress string serviceTaggedAddresses map[string]structs.ServiceAddress expectedServiceIPv4Address string expectedServiceIPv6Address string expectedNodeIPv4Address string expectedNodeIPv6Address string } cases := map[string]testCase{ "simple-ipv4": { serviceAddress: "127.0.0.2", nodeAddress: "127.0.0.1", expectedServiceIPv4Address: "127.0.0.2", expectedServiceIPv6Address: "", expectedNodeIPv4Address: "127.0.0.1", expectedNodeIPv6Address: "", }, "simple-ipv6": { serviceAddress: "::2", nodeAddress: "::1", expectedServiceIPv6Address: "::2", expectedServiceIPv4Address: "", expectedNodeIPv6Address: "::1", expectedNodeIPv4Address: "", }, "ipv4-with-tagged-ipv6": { serviceAddress: "127.0.0.2", nodeAddress: "127.0.0.1", serviceTaggedAddresses: map[string]structs.ServiceAddress{ structs.TaggedAddressLANIPv6: {Address: "::2"}, }, nodeTaggedAddresses: map[string]string{ structs.TaggedAddressLANIPv6: "::1", }, expectedServiceIPv4Address: "127.0.0.2", expectedServiceIPv6Address: "::2", expectedNodeIPv4Address: "127.0.0.1", expectedNodeIPv6Address: "::1", }, "ipv6-with-tagged-ipv4": { serviceAddress: "::2", nodeAddress: "::1", serviceTaggedAddresses: map[string]structs.ServiceAddress{ structs.TaggedAddressLANIPv4: {Address: "127.0.0.2"}, }, nodeTaggedAddresses: map[string]string{ structs.TaggedAddressLANIPv4: "127.0.0.1", }, expectedServiceIPv4Address: "127.0.0.2", expectedServiceIPv6Address: "::2", expectedNodeIPv4Address: "127.0.0.1", expectedNodeIPv6Address: "::1", }, } for name, tc := range cases { name := name tc := tc t.Run(name, func(t *testing.T) { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "foo", Address: tc.nodeAddress, TaggedAddresses: tc.nodeTaggedAddresses, Service: &structs.NodeService{ Service: "db", Address: tc.serviceAddress, Port: 8080, TaggedAddresses: tc.serviceTaggedAddresses, }, } var out struct{} require.NoError(t, a.RPC(context.Background(), "Catalog.Register", args, &out)) // Look up the SRV record via service and prepared query. questions := []string{ "db.service.consul.", id + ".query.consul.", } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question, dns.TypeA) c := new(dns.Client) addr := a.config.DNSAddrs[0].String() in, _, err := c.Exchange(m, addr) require.NoError(t, err) if tc.expectedServiceIPv4Address != "" { require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok, "Bad: %#v", in.Answer[0]) require.Equal(t, question, aRec.Hdr.Name) require.Equal(t, tc.expectedServiceIPv4Address, aRec.A.String()) } else { require.Len(t, in.Answer, 0) } m = new(dns.Msg) m.SetQuestion(question, dns.TypeAAAA) c = new(dns.Client) addr = a.config.DNSAddrs[0].String() in, _, err = c.Exchange(m, addr) require.NoError(t, err) if tc.expectedServiceIPv6Address != "" { require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.AAAA) require.True(t, ok, "Bad: %#v", in.Answer[0]) require.Equal(t, question, aRec.Hdr.Name) require.Equal(t, tc.expectedServiceIPv6Address, aRec.AAAA.String()) } else { require.Len(t, in.Answer, 0) } } // Look up node m := new(dns.Msg) m.SetQuestion("foo.node.consul.", dns.TypeA) c := new(dns.Client) addr := a.config.DNSAddrs[0].String() in, _, err := c.Exchange(m, addr) require.NoError(t, err) if tc.expectedNodeIPv4Address != "" { require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok, "Bad: %#v", in.Answer[0]) require.Equal(t, "foo.node.consul.", aRec.Hdr.Name) require.Equal(t, tc.expectedNodeIPv4Address, aRec.A.String()) } else { require.Len(t, in.Answer, 0) } m = new(dns.Msg) m.SetQuestion("foo.node.consul.", dns.TypeAAAA) c = new(dns.Client) addr = a.config.DNSAddrs[0].String() in, _, err = c.Exchange(m, addr) require.NoError(t, err) if tc.expectedNodeIPv6Address != "" { require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.AAAA) require.True(t, ok, "Bad: %#v", in.Answer[0]) require.Equal(t, "foo.node.consul.", aRec.Hdr.Name) require.Equal(t, tc.expectedNodeIPv6Address, aRec.AAAA.String()) } else { require.Len(t, in.Answer, 0) } }) } }) } } func TestDNS_PreparedQueryNearIPEDNS(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } ipCoord := librtt.GenerateCoordinate(1 * time.Millisecond) serviceNodes := []struct { name string address string coord *coordinate.Coordinate }{ {"foo1", "198.18.0.1", librtt.GenerateCoordinate(1 * time.Millisecond)}, {"foo2", "198.18.0.2", librtt.GenerateCoordinate(10 * time.Millisecond)}, {"foo3", "198.18.0.3", librtt.GenerateCoordinate(30 * time.Millisecond)}, } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") added := 0 // Register nodes with a service for _, cfg := range serviceNodes { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: cfg.name, Address: cfg.address, Service: &structs.NodeService{ Service: "db", Port: 12345, }, } var out struct{} err := a.RPC(context.Background(), "Catalog.Register", args, &out) require.NoError(t, err) // Send coordinate updates coordArgs := structs.CoordinateUpdateRequest{ Datacenter: "dc1", Node: cfg.name, Coord: cfg.coord, } err = a.RPC(context.Background(), "Coordinate.Update", &coordArgs, &out) require.NoError(t, err) added += 1 } fmt.Printf("Added %d service nodes\n", added) // Register a node without a service { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "bar", Address: "198.18.0.9", } var out struct{} err := a.RPC(context.Background(), "Catalog.Register", args, &out) require.NoError(t, err) // Send coordinate updates for a few nodes. coordArgs := structs.CoordinateUpdateRequest{ Datacenter: "dc1", Node: "bar", Coord: ipCoord, } err = a.RPC(context.Background(), "Coordinate.Update", &coordArgs, &out) require.NoError(t, err) } // Register a prepared query Near = _ip { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "some.query.we.like", Service: structs.ServiceQuery{ Service: "db", Near: "_ip", }, }, } var id string err := a.RPC(context.Background(), "PreparedQuery.Apply", args, &id) require.NoError(t, err) } retry.Run(t, func(r *retry.R) { m := new(dns.Msg) m.SetQuestion("some.query.we.like.query.consul.", dns.TypeA) m.SetEdns0(4096, false) o := new(dns.OPT) o.Hdr.Name = "." o.Hdr.Rrtype = dns.TypeOPT e := new(dns.EDNS0_SUBNET) e.Code = dns.EDNS0SUBNET e.Family = 1 e.SourceNetmask = 32 e.SourceScope = 0 e.Address = net.ParseIP("198.18.0.9").To4() o.Option = append(o.Option, e) m.Extra = append(m.Extra, o) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { r.Fatalf("Error with call to dns.Client.Exchange: %s", err) } if len(serviceNodes) != len(in.Answer) { r.Fatalf("Expecting %d A RRs in response, Actual found was %d", len(serviceNodes), len(in.Answer)) } for i, rr := range in.Answer { if aRec, ok := rr.(*dns.A); ok { if actual := aRec.A.String(); serviceNodes[i].address != actual { r.Fatalf("Expecting A RR #%d = %s, Actual RR was %s", i, serviceNodes[i].address, actual) } } else { r.Fatalf("DNS Answer contained a non-A RR") } } }) }) } } func TestDNS_PreparedQueryNearIP(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } ipCoord := librtt.GenerateCoordinate(1 * time.Millisecond) serviceNodes := []struct { name string address string coord *coordinate.Coordinate }{ {"foo1", "198.18.0.1", librtt.GenerateCoordinate(1 * time.Millisecond)}, {"foo2", "198.18.0.2", librtt.GenerateCoordinate(10 * time.Millisecond)}, {"foo3", "198.18.0.3", librtt.GenerateCoordinate(30 * time.Millisecond)}, } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") added := 0 // Register nodes with a service for _, cfg := range serviceNodes { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: cfg.name, Address: cfg.address, Service: &structs.NodeService{ Service: "db", Port: 12345, }, } var out struct{} err := a.RPC(context.Background(), "Catalog.Register", args, &out) require.NoError(t, err) // Send coordinate updates coordArgs := structs.CoordinateUpdateRequest{ Datacenter: "dc1", Node: cfg.name, Coord: cfg.coord, } err = a.RPC(context.Background(), "Coordinate.Update", &coordArgs, &out) require.NoError(t, err) added += 1 } fmt.Printf("Added %d service nodes\n", added) // Register a node without a service { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "bar", Address: "198.18.0.9", } var out struct{} err := a.RPC(context.Background(), "Catalog.Register", args, &out) require.NoError(t, err) // Send coordinate updates for a few nodes. coordArgs := structs.CoordinateUpdateRequest{ Datacenter: "dc1", Node: "bar", Coord: ipCoord, } err = a.RPC(context.Background(), "Coordinate.Update", &coordArgs, &out) require.NoError(t, err) } // Register a prepared query Near = _ip { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "some.query.we.like", Service: structs.ServiceQuery{ Service: "db", Near: "_ip", }, }, } var id string err := a.RPC(context.Background(), "PreparedQuery.Apply", args, &id) require.NoError(t, err) } retry.Run(t, func(r *retry.R) { m := new(dns.Msg) m.SetQuestion("some.query.we.like.query.consul.", dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { r.Fatalf("Error with call to dns.Client.Exchange: %s", err) } if len(serviceNodes) != len(in.Answer) { r.Fatalf("Expecting %d A RRs in response, Actual found was %d", len(serviceNodes), len(in.Answer)) } for i, rr := range in.Answer { if aRec, ok := rr.(*dns.A); ok { if actual := aRec.A.String(); serviceNodes[i].address != actual { r.Fatalf("Expecting A RR #%d = %s, Actual RR was %s", i, serviceNodes[i].address, actual) } } else { r.Fatalf("DNS Answer contained a non-A RR") } } }) }) } } func TestDNS_Recurse(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } recursor := makeRecursor(t, dns.Msg{ Answer: []dns.RR{dnsA("apple.com", "1.2.3.4")}, }) defer recursor.Shutdown() for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` recursors = ["`+recursor.Addr+`"] `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("apple.com.", dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) == 0 { t.Fatalf("Bad: %#v", in) } if in.Rcode != dns.RcodeSuccess { t.Fatalf("Bad: %#v", in) } }) } } func TestDNS_Recurse_Truncation(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } recursor := makeRecursor(t, dns.Msg{ MsgHdr: dns.MsgHdr{Truncated: true}, Answer: []dns.RR{dnsA("apple.com", "1.2.3.4")}, }) defer recursor.Shutdown() for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` recursors = ["`+recursor.Addr+`"] `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("apple.com.", dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if in.Truncated != true { t.Fatalf("err: message should have been truncated %v", in) } if len(in.Answer) == 0 { t.Fatalf("Bad: Truncated message ignored, expected some reply %#v", in) } if in.Rcode != dns.RcodeSuccess { t.Fatalf("Bad: %#v", in) } }) } } func TestDNS_RecursorTimeout(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } serverClientTimeout := 3 * time.Second testClientTimeout := serverClientTimeout + 5*time.Second resolverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") if err != nil { t.Error(err) } resolver, err := net.ListenUDP("udp", resolverAddr) if err != nil { t.Error(err) } defer resolver.Close() for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` recursors = ["`+resolver.LocalAddr().String()+`"] // host must cause a connection|read|write timeout dns_config { recursor_timeout = "`+serverClientTimeout.String()+`" } `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("apple.com.", dns.TypeANY) // This client calling the server under test must have a longer timeout than the one we set internally c := &dns.Client{Timeout: testClientTimeout} start := time.Now() in, _, err := c.Exchange(m, a.DNSAddr()) duration := time.Since(start) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 0 { t.Fatalf("Bad: %#v", in) } if in.Rcode != dns.RcodeServerFailure { t.Fatalf("Bad: %#v", in) } if duration < serverClientTimeout { t.Fatalf("Expected the call to return after at least %f seconds but lasted only %f", serverClientTimeout.Seconds(), duration.Seconds()) } }) } } // no way to run a v2 version of this test since it is calling a private function and not // using a test agent. func TestDNS_BinarySearch(t *testing.T) { msgSrc := new(dns.Msg) msgSrc.Compress = true msgSrc.SetQuestion("redis.service.consul.", dns.TypeSRV) for i := 0; i < 5000; i++ { target := fmt.Sprintf("host-redis-%d-%d.test.acme.com.node.dc1.consul.", i/256, i%256) msgSrc.Answer = append(msgSrc.Answer, &dns.SRV{Hdr: dns.RR_Header{Name: "redis.service.consul.", Class: 1, Rrtype: dns.TypeSRV, Ttl: 0x3c}, Port: 0x4c57, Target: target}) msgSrc.Extra = append(msgSrc.Extra, &dns.CNAME{Hdr: dns.RR_Header{Name: target, Class: 1, Rrtype: dns.TypeCNAME, Ttl: 0x3c}, Target: fmt.Sprintf("fx.168.%d.%d.", i/256, i%256)}) } for _, compress := range []bool{true, false} { for idx, maxSize := range []int{12, 256, 512, 8192, 65535} { t.Run(fmt.Sprintf("binarySearch %d", maxSize), func(t *testing.T) { msg := new(dns.Msg) msgSrc.Compress = compress msgSrc.SetQuestion("redis.service.consul.", dns.TypeSRV) msg.Answer = msgSrc.Answer msg.Extra = msgSrc.Extra msg.Ns = msgSrc.Ns index := make(map[string]dns.RR, len(msg.Extra)) indexRRs(msg.Extra, index) blen := dnsBinaryTruncate(msg, maxSize, index, true) msg.Answer = msg.Answer[:blen] syncExtra(index, msg) predicted := msg.Len() buf, err := msg.Pack() if err != nil { t.Error(err) } if predicted < len(buf) { t.Fatalf("Bug in DNS library: %d != %d", predicted, len(buf)) } if len(buf) > maxSize || (idx != 0 && len(buf) < 16) { t.Fatalf("bad[%d]: %d > %d", idx, len(buf), maxSize) } }) } } } func TestDNS_TCP_and_UDP_Truncate(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` dns_config { enable_truncate = true } `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") services := []string{"normal", "truncated"} for index, service := range services { numServices := (index * 5000) + 2 var eg errgroup.Group for i := 1; i < numServices; i++ { j := i eg.Go(func() error { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: fmt.Sprintf("%s-%d.acme.com", service, j), Address: fmt.Sprintf("127.%d.%d.%d", 0, (j / 255), j%255), Service: &structs.NodeService{ Service: service, Port: 8000, }, } var out struct{} return a.RPC(context.Background(), "Catalog.Register", args, &out) }) } if err := eg.Wait(); err != nil { t.Fatalf("error registering: %v", err) } // Register an equivalent prepared query. var id string { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: service, Service: structs.ServiceQuery{ Service: service, }, }, } if err := a.RPC(context.Background(), "PreparedQuery.Apply", args, &id); err != nil { t.Fatalf("err: %v", err) } } // Look up the service directly and via prepared query. Ensure the // response is truncated each time. questions := []string{ fmt.Sprintf("%s.service.consul.", service), id + ".query.consul.", } protocols := []string{ "tcp", "udp", } for _, maxSize := range []uint16{8192, 65535} { for _, qType := range []uint16{dns.TypeANY, dns.TypeA, dns.TypeSRV} { for _, question := range questions { for _, protocol := range protocols { for _, compress := range []bool{true, false} { t.Run(fmt.Sprintf("lookup %s %s (qType:=%d) compressed=%v", question, protocol, qType, compress), func(t *testing.T) { m := new(dns.Msg) m.SetQuestion(question, dns.TypeANY) maxSz := maxSize if protocol == "udp" { maxSz = 8192 } m.SetEdns0(maxSz, true) c := new(dns.Client) c.Net = protocol m.Compress = compress in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } // actually check if we need to have the truncate bit resbuf, err := in.Pack() if err != nil { t.Fatalf("Error while packing answer: %s", err) } if !in.Truncated && len(resbuf) > int(maxSz) { t.Fatalf("should have truncate bit %#v %#v", in, len(in.Answer)) } // Check for the truncate bit buf, err := m.Pack() info := fmt.Sprintf("service %s question:=%s (%s) (%d total records) sz:= %d in %v", service, question, protocol, numServices, len(in.Answer), in) if err != nil { t.Fatalf("Error while packing: %v ; info:=%s", err, info) } if len(buf) > int(maxSz) { t.Fatalf("len(buf) := %d > maxSz=%d for %v", len(buf), maxSz, info) } }) } } } } } } }) } } func TestDNS_AddressLookup(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Look up the addresses cases := map[string]string{ "7f000001.addr.dc1.consul.": "127.0.0.1", } for question, answer := range cases { m := new(dns.Msg) m.SetQuestion(question, dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } require.Len(t, in.Answer, 1) require.Equal(t, dns.TypeA, in.Answer[0].Header().Rrtype) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok) require.Equal(t, aRec.A.To4().String(), answer) require.Zero(t, aRec.Hdr.Ttl) require.Nil(t, in.Ns) require.Nil(t, in.Extra) } }) } } func TestDNS_AddressLookupANY(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Look up the addresses cases := map[string]string{ "7f000001.addr.dc1.consul.": "127.0.0.1", } for question, answer := range cases { m := new(dns.Msg) m.SetQuestion(question, dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) require.NoError(t, err) require.Len(t, in.Answer, 1) require.Equal(t, in.Answer[0].Header().Rrtype, dns.TypeA) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok) require.Equal(t, aRec.A.To4().String(), answer) require.Zero(t, aRec.Hdr.Ttl) } }) } } func TestDNS_AddressLookupInvalidType(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Look up the addresses cases := map[string]string{ "7f000001.addr.dc1.consul.": "", } for question := range cases { m := new(dns.Msg) m.SetQuestion(question, dns.TypeSRV) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) require.NoError(t, err) require.Zero(t, in.Rcode) require.Nil(t, in.Answer) require.NotNil(t, in.Extra) require.Len(t, in.Extra, 1) aRecord := in.Extra[0].(*dns.A) require.Equal(t, "7f000001.addr.dc1.consul.", aRecord.Hdr.Name) require.Equal(t, dns.TypeA, aRecord.Hdr.Rrtype) require.Zero(t, aRecord.Hdr.Ttl) require.Equal(t, "127.0.0.1", aRecord.A.String()) } }) } } func TestDNS_AddressLookupIPV6(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Look up the addresses cases := map[string]string{ "2607002040050808000000000000200e.addr.consul.": "2607:20:4005:808::200e", "2607112040051808ffffffffffff200e.addr.consul.": "2607:1120:4005:1808:ffff:ffff:ffff:200e", } for question, answer := range cases { m := new(dns.Msg) m.SetQuestion(question, dns.TypeAAAA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 1 { t.Fatalf("Bad: %#v", in) } if in.Answer[0].Header().Rrtype != dns.TypeAAAA { t.Fatalf("Invalid type: %#v", in.Answer[0]) } aaaaRec, ok := in.Answer[0].(*dns.AAAA) if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } if aaaaRec.AAAA.To16().String() != answer { t.Fatalf("Bad: %#v", aaaaRec) } if aaaaRec.Hdr.Ttl != 0 { t.Fatalf("Bad: %#v", in.Answer[0]) } } }) } } func TestDNS_AddressLookupIPV6InvalidType(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Look up the addresses cases := map[string]string{ "2607002040050808000000000000200e.addr.consul.": "2607:20:4005:808::200e", "2607112040051808ffffffffffff200e.addr.consul.": "2607:1120:4005:1808:ffff:ffff:ffff:200e", } for question := range cases { m := new(dns.Msg) m.SetQuestion(question, dns.TypeSRV) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if in.Answer != nil { t.Fatalf("Bad: %#v", in) } } }) } } // TestDNS_NonExistentDC_Server verifies NXDOMAIN is returned when // Consul server agent is queried for a service in a non-existent // domain. func TestDNS_NonExistentDC_Server(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("consul.service.dc2.consul.", dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } require.Equal(t, dns.RcodeNameError, in.Rcode) require.Equal(t, 0, len(in.Answer)) require.Equal(t, 0, len(in.Extra)) require.Equal(t, 1, len(in.Ns)) soa := in.Ns[0].(*dns.SOA) require.Equal(t, "consul.", soa.Hdr.Name) require.Equal(t, "ns.consul.", soa.Ns) require.Equal(t, "hostmaster.consul.", soa.Mbox) }) } } // TestDNS_NonExistentDC_RPC verifies NXDOMAIN is returned when // Consul server agent is queried over RPC by a non-server agent // for a service in a non-existent domain func TestDNS_NonExistentDC_RPC(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { s := NewTestAgent(t, ` node_name = "test-server" `+experimentsHCL) defer s.Shutdown() c := NewTestAgent(t, ` node_name = "test-client" bootstrap = false server = false `+experimentsHCL) defer c.Shutdown() // Join LAN cluster addr := fmt.Sprintf("127.0.0.1:%d", s.Config.SerfPortLAN) _, err := c.JoinLAN([]string{addr}, nil) require.NoError(t, err) testrpc.WaitForTestAgent(t, c.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("consul.service.dc2.consul.", dns.TypeANY) d := new(dns.Client) in, _, err := d.Exchange(m, c.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if in.Rcode != dns.RcodeNameError { t.Fatalf("Expected RCode: %#v, had: %#v", dns.RcodeNameError, in.Rcode) } }) } } func TestDNS_NonExistentLookup(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // lookup a non-existing node, we should receive a SOA m := new(dns.Msg) m.SetQuestion("nonexisting.consul.", dns.TypeANY) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Ns) != 1 { t.Fatalf("Bad: %#v %#v", in, len(in.Answer)) } soaRec, ok := in.Ns[0].(*dns.SOA) if !ok { t.Fatalf("Bad: %#v", in.Ns[0]) } if soaRec.Hdr.Ttl != 0 { t.Fatalf("Bad: %#v", in.Ns[0]) } }) } } func TestDNS_NonExistentLookupEmptyAorAAAA(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register a v6-only service and a v4-only service. { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "foov6", Address: "fe80::1", Service: &structs.NodeService{ Service: "webv6", Port: 8000, }, } var out struct{} if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } args = &structs.RegisterRequest{ Datacenter: "dc1", Node: "foov4", Address: "127.0.0.1", Service: &structs.NodeService{ Service: "webv4", Port: 8000, }, } if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } } // Register equivalent prepared queries. { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "webv4", Service: structs.ServiceQuery{ Service: "webv4", }, }, } var id string if err := a.RPC(context.Background(), "PreparedQuery.Apply", args, &id); err != nil { t.Fatalf("err: %v", err) } args = &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "webv6", Service: structs.ServiceQuery{ Service: "webv6", }, }, } if err := a.RPC(context.Background(), "PreparedQuery.Apply", args, &id); err != nil { t.Fatalf("err: %v", err) } } // Check for ipv6 records on ipv4-only service directly and via the // prepared query. questions := []string{ "webv4.service.consul.", "webv4.query.consul.", } for _, question := range questions { t.Run(question, func(t *testing.T) { m := new(dns.Msg) m.SetQuestion(question, dns.TypeAAAA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } require.Len(t, in.Ns, 1) soaRec, ok := in.Ns[0].(*dns.SOA) if !ok { t.Fatalf("Bad: %#v", in.Ns[0]) } if soaRec.Hdr.Ttl != 0 { t.Fatalf("Bad: %#v", in.Ns[0]) } require.Equal(t, dns.RcodeSuccess, in.Rcode) }) } // Check for ipv4 records on ipv6-only service directly and via the // prepared query. questions = []string{ "webv6.service.consul.", "webv6.query.consul.", } for _, question := range questions { t.Run(question, func(t *testing.T) { m := new(dns.Msg) m.SetQuestion(question, dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Ns) != 1 { t.Fatalf("Bad: %#v", in) } soaRec, ok := in.Ns[0].(*dns.SOA) if !ok { t.Fatalf("Bad: %#v", in.Ns[0]) } if soaRec.Hdr.Ttl != 0 { t.Fatalf("Bad: %#v", in.Ns[0]) } if in.Rcode != dns.RcodeSuccess { t.Fatalf("Bad: %#v", in) } }) } }) } } func TestDNS_AltDomains_Service(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` alt_domain = "test-domain." `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register a node with a service. { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "test-node", Address: "127.0.0.1", Service: &structs.NodeService{ Service: "db", Tags: []string{"primary"}, Port: 12345, }, NodeMeta: map[string]string{ "key": "value", }, } var out struct{} if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } } questions := []struct { ask string wantDomain string }{ {"db.service.consul.", "test-node.node.dc1.consul."}, {"db.service.test-domain.", "test-node.node.dc1.test-domain."}, {"db.service.dc1.consul.", "test-node.node.dc1.consul."}, {"db.service.dc1.test-domain.", "test-node.node.dc1.test-domain."}, } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question.ask, dns.TypeSRV) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 1 { t.Fatalf("Bad: %#v", in) } srvRec, ok := in.Answer[0].(*dns.SRV) if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } if srvRec.Port != 12345 { t.Fatalf("Bad: %#v", srvRec) } if got, want := srvRec.Target, question.wantDomain; got != want { t.Fatalf("SRV target invalid, got %v want %v", got, want) } aRec, ok := in.Extra[0].(*dns.A) if !ok { t.Fatalf("Bad: %#v", in.Extra[0]) } if got, want := aRec.Hdr.Name, question.wantDomain; got != want { t.Fatalf("A record header invalid, got %v want %v", got, want) } if aRec.A.String() != "127.0.0.1" { t.Fatalf("Bad: %#v", in.Extra[0]) } txtRec, ok := in.Extra[1].(*dns.TXT) if !ok { t.Fatalf("Bad: %#v", in.Extra[1]) } if got, want := txtRec.Hdr.Name, question.wantDomain; got != want { t.Fatalf("TXT record header invalid, got %v want %v", got, want) } if txtRec.Txt[0] != "key=value" { t.Fatalf("Bad: %#v", in.Extra[1]) } } }) } } func TestDNS_AltDomains_SOA(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` node_name = "test-node" alt_domain = "test-domain." `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") questions := []struct { ask string want_domain string }{ {"test-node.node.consul.", "consul."}, {"test-node.node.test-domain.", "test-domain."}, } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question.ask, dns.TypeSOA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 1 { t.Fatalf("Bad: %#v", in) } soaRec, ok := in.Answer[0].(*dns.SOA) if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } if got, want := soaRec.Hdr.Name, question.want_domain; got != want { t.Fatalf("SOA name invalid, got %q want %q", got, want) } if got, want := soaRec.Ns, ("ns." + question.want_domain); got != want { t.Fatalf("SOA ns invalid, got %q want %q", got, want) } } }) } } func TestDNS_AltDomains_Overlap(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } // this tests the domain matching logic in DNSServer when encountering more // than one potential match (i.e. ambiguous match) // it should select the longer matching domain when dispatching for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` node_name = "test-node" alt_domain = "test.consul." `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") questions := []string{ "test-node.node.consul.", "test-node.node.test.consul.", "test-node.node.dc1.consul.", "test-node.node.dc1.test.consul.", } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question, dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Answer) != 1 { t.Fatalf("failed to resolve ambiguous alt domain %q: %#v", question, in) } aRec, ok := in.Answer[0].(*dns.A) if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } if got, want := aRec.A.To4().String(), "127.0.0.1"; got != want { t.Fatalf("A ip invalid, got %v want %v", got, want) } } }) } } func TestDNS_AltDomain_DCName_Overlap(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } // this tests the DC name overlap with the consul domain/alt-domain // we should get response when DC suffix is a prefix of consul alt-domain for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` datacenter = "dc-test" node_name = "test-node" alt_domain = "test.consul." `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc-test") questions := []string{ "test-node.node.dc-test.consul.", "test-node.node.dc-test.test.consul.", } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question, dns.TypeA) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } require.Len(t, in.Answer, 1) aRec, ok := in.Answer[0].(*dns.A) require.True(t, ok) require.Equal(t, aRec.A.To4().String(), "127.0.0.1") } }) } } func TestDNS_PreparedQuery_AllowStale(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` dns_config { allow_stale = true max_stale = "1s" } `+experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := MockPreparedQuery{ executeFn: func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { // Return a response that's perpetually too stale. reply.LastContact = 2 * time.Second return nil }, } if err := a.registerEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } // Make sure that the lookup terminates and results in an SOA since // the query doesn't exist. { m := new(dns.Msg) m.SetQuestion("nope.query.consul.", dns.TypeSRV) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Ns) != 1 { t.Fatalf("Bad: %#v", in) } soaRec, ok := in.Ns[0].(*dns.SOA) if !ok { t.Fatalf("Bad: %#v", in.Ns[0]) } if soaRec.Hdr.Ttl != 0 { t.Fatalf("Bad: %#v", in.Ns[0]) } } }) } } func TestDNS_InvalidQueries(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Try invalid forms of queries that should hit the special invalid case // of our query parser. questions := []string{ "consul.", "node.consul.", "service.consul.", "query.consul.", "foo.node.dc1.extra.more.consul.", "foo.service.dc1.extra.more.consul.", "foo.query.dc1.extra.more.consul.", } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question, dns.TypeSRV) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } if len(in.Ns) != 1 { t.Fatalf("Bad: %#v", in) } soaRec, ok := in.Ns[0].(*dns.SOA) if !ok { t.Fatalf("Bad: %#v", in.Ns[0]) } if soaRec.Hdr.Ttl != 0 { t.Fatalf("Bad: %#v", in.Ns[0]) } } }) } } func TestDNS_PreparedQuery_AgentSource(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := MockPreparedQuery{ executeFn: func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { // Check that the agent inserted its self-name and datacenter to // the RPC request body. if args.Agent.Datacenter != a.Config.Datacenter || args.Agent.Node != a.Config.NodeName { t.Fatalf("bad: %#v", args.Agent) } return nil }, } if err := a.registerEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } { m := new(dns.Msg) m.SetQuestion("foo.query.consul.", dns.TypeSRV) c := new(dns.Client) if _, _, err := c.Exchange(m, a.DNSAddr()); err != nil { t.Fatalf("err: %v", err) } } }) } } func TestDNS_EDNS_Truncate_AgentSource(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` dns_config { enable_truncate = true } `+experimentsHCL) defer a.Shutdown() a.DNSDisableCompression(true) testrpc.WaitForLeader(t, a.RPC, "dc1") m := MockPreparedQuery{ executeFn: func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { // Check that the agent inserted its self-name and datacenter to // the RPC request body. if args.Agent.Datacenter != a.Config.Datacenter || args.Agent.Node != a.Config.NodeName { t.Fatalf("bad: %#v", args.Agent) } for i := 0; i < 100; i++ { reply.Nodes = append(reply.Nodes, structs.CheckServiceNode{Node: &structs.Node{Node: "apple", Address: fmt.Sprintf("node.address:%d", i)}, Service: &structs.NodeService{Service: "appleService", Address: fmt.Sprintf("service.address:%d", i)}}) } return nil }, } if err := a.registerEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } req := new(dns.Msg) req.SetQuestion("foo.query.consul.", dns.TypeSRV) req.SetEdns0(2048, true) req.Compress = false c := new(dns.Client) resp, _, err := c.Exchange(req, a.DNSAddr()) require.NoError(t, err) require.True(t, resp.Len() < 2048) }) } } func TestDNS_trimUDPResponse_NoTrim(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { req := &dns.Msg{} resp := &dns.Msg{ Answer: []dns.RR{ &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: "ip-10-0-1-185.node.dc1.consul.", }, }, Extra: []dns.RR{ &dns.A{ Hdr: dns.RR_Header{ Name: "ip-10-0-1-185.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("10.0.1.185"), }, }, } cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) if trimmed := trimUDPResponse(req, resp, cfg.DNSUDPAnswerLimit); trimmed { t.Fatalf("Bad %#v", *resp) } expected := &dns.Msg{ Answer: []dns.RR{ &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: "ip-10-0-1-185.node.dc1.consul.", }, }, Extra: []dns.RR{ &dns.A{ Hdr: dns.RR_Header{ Name: "ip-10-0-1-185.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("10.0.1.185"), }, }, } if !reflect.DeepEqual(resp, expected) { t.Fatalf("Bad %#v vs. %#v", *resp, *expected) } }) } } func TestDNS_trimUDPResponse_TrimLimit(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) req, resp, expected := &dns.Msg{}, &dns.Msg{}, &dns.Msg{} for i := 0; i < cfg.DNSUDPAnswerLimit+1; i++ { target := fmt.Sprintf("ip-10-0-1-%d.node.dc1.consul.", 185+i) srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: target, } a := &dns.A{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(fmt.Sprintf("10.0.1.%d", 185+i)), } resp.Answer = append(resp.Answer, srv) resp.Extra = append(resp.Extra, a) if i < cfg.DNSUDPAnswerLimit { expected.Answer = append(expected.Answer, srv) expected.Extra = append(expected.Extra, a) } } if trimmed := trimUDPResponse(req, resp, cfg.DNSUDPAnswerLimit); !trimmed { t.Fatalf("Bad %#v", *resp) } if !reflect.DeepEqual(resp, expected) { t.Fatalf("Bad %#v vs. %#v", *resp, *expected) } }) } } func TestDNS_trimUDPResponse_TrimLimitWithNS(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) req, resp, expected := &dns.Msg{}, &dns.Msg{}, &dns.Msg{} for i := 0; i < cfg.DNSUDPAnswerLimit+1; i++ { target := fmt.Sprintf("ip-10-0-1-%d.node.dc1.consul.", 185+i) srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: target, } a := &dns.A{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(fmt.Sprintf("10.0.1.%d", 185+i)), } ns := &dns.SOA{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeSOA, Class: dns.ClassINET, }, Ns: fmt.Sprintf("soa-%d", i), } resp.Answer = append(resp.Answer, srv) resp.Extra = append(resp.Extra, a) resp.Ns = append(resp.Ns, ns) if i < cfg.DNSUDPAnswerLimit { expected.Answer = append(expected.Answer, srv) expected.Extra = append(expected.Extra, a) } } if trimmed := trimUDPResponse(req, resp, cfg.DNSUDPAnswerLimit); !trimmed { t.Fatalf("Bad %#v", *resp) } require.LessOrEqual(t, resp.Len(), defaultMaxUDPSize) require.Len(t, resp.Ns, 0) }) } } func TestDNS_trimTCPResponse_TrimLimitWithNS(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) req, resp, expected := &dns.Msg{}, &dns.Msg{}, &dns.Msg{} for i := 0; i < 5000; i++ { target := fmt.Sprintf("ip-10-0-1-%d.node.dc1.consul.", 185+i) srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: target, } a := &dns.A{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(fmt.Sprintf("10.0.1.%d", 185+i)), } ns := &dns.SOA{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeSOA, Class: dns.ClassINET, }, Ns: fmt.Sprintf("soa-%d", i), } resp.Answer = append(resp.Answer, srv) resp.Extra = append(resp.Extra, a) resp.Ns = append(resp.Ns, ns) if i < cfg.DNSUDPAnswerLimit { expected.Answer = append(expected.Answer, srv) expected.Extra = append(expected.Extra, a) } } req.Question = append(req.Question, dns.Question{Qtype: dns.TypeSRV}) if trimmed := trimTCPResponse(req, resp); !trimmed { t.Fatalf("Bad %#v", *resp) } require.LessOrEqual(t, resp.Len(), 65523) require.Len(t, resp.Ns, 0) }) } } func loadRuntimeConfig(t *testing.T, hcl string) *config.RuntimeConfig { t.Helper() result, err := config.Load(config.LoadOpts{HCL: []string{hcl}}) require.NoError(t, err) require.Len(t, result.Warnings, 0) return result.RuntimeConfig } func TestDNS_trimUDPResponse_TrimSize(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) req, resp := &dns.Msg{}, &dns.Msg{} for i := 0; i < 100; i++ { target := fmt.Sprintf("ip-10-0-1-%d.node.dc1.consul.", 185+i) srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: target, } a := &dns.A{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(fmt.Sprintf("10.0.1.%d", 185+i)), } resp.Answer = append(resp.Answer, srv) resp.Extra = append(resp.Extra, a) } // We don't know the exact trim, but we know the resulting answer // data should match its extra data. if trimmed := trimUDPResponse(req, resp, cfg.DNSUDPAnswerLimit); !trimmed { t.Fatalf("Bad %#v", *resp) } if len(resp.Answer) == 0 || len(resp.Answer) != len(resp.Extra) { t.Fatalf("Bad %#v", *resp) } for i := range resp.Answer { srv, ok := resp.Answer[i].(*dns.SRV) if !ok { t.Fatalf("should be SRV") } a, ok := resp.Extra[i].(*dns.A) if !ok { t.Fatalf("should be A") } if srv.Target != a.Header().Name { t.Fatalf("Bad %#v vs. %#v", *srv, *a) } } }) } } func TestDNS_trimUDPResponse_TrimSizeEDNS(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) req, resp := &dns.Msg{}, &dns.Msg{} for i := 0; i < 100; i++ { target := fmt.Sprintf("ip-10-0-1-%d.node.dc1.consul.", 150+i) srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: target, } a := &dns.A{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(fmt.Sprintf("10.0.1.%d", 150+i)), } resp.Answer = append(resp.Answer, srv) resp.Extra = append(resp.Extra, a) } // Copy over to a new slice since we are trimming both. reqEDNS, respEDNS := &dns.Msg{}, &dns.Msg{} reqEDNS.SetEdns0(2048, true) respEDNS.Answer = append(respEDNS.Answer, resp.Answer...) respEDNS.Extra = append(respEDNS.Extra, resp.Extra...) // Trim each response if trimmed := trimUDPResponse(req, resp, cfg.DNSUDPAnswerLimit); !trimmed { t.Errorf("expected response to be trimmed: %#v", resp) } if trimmed := trimUDPResponse(reqEDNS, respEDNS, cfg.DNSUDPAnswerLimit); !trimmed { t.Errorf("expected edns to be trimmed: %#v", resp) } // Check answer lengths if len(resp.Answer) == 0 || len(resp.Answer) != len(resp.Extra) { t.Errorf("bad response answer length: %#v", resp) } if len(respEDNS.Answer) == 0 || len(respEDNS.Answer) != len(respEDNS.Extra) { t.Errorf("bad edns answer length: %#v", resp) } // Due to the compression, we can't check exact equality of sizes, but we can // make two requests and ensure that the edns one returns a larger payload // than the non-edns0 one. if len(resp.Answer) >= len(respEDNS.Answer) { t.Errorf("expected edns have larger answer: %#v\n%#v", resp, respEDNS) } if len(resp.Extra) >= len(respEDNS.Extra) { t.Errorf("expected edns have larger extra: %#v\n%#v", resp, respEDNS) } // Verify that the things point where they should for i := range resp.Answer { srv, ok := resp.Answer[i].(*dns.SRV) if !ok { t.Errorf("%d should be an SRV", i) } a, ok := resp.Extra[i].(*dns.A) if !ok { t.Errorf("%d should be an A", i) } if srv.Target != a.Header().Name { t.Errorf("%d: bad %#v vs. %#v", i, srv, a) } } }) } } func TestDNS_trimUDPResponse_TrimSizeMaxSize(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `node_name = "test" data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) resp := &dns.Msg{} for i := 0; i < 600; i++ { target := fmt.Sprintf("ip-10-0-1-%d.node.dc1.consul.", 150+i) srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Target: target, } a := &dns.A{ Hdr: dns.RR_Header{ Name: target, Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP(fmt.Sprintf("10.0.1.%d", 150+i)), } resp.Answer = append(resp.Answer, srv) resp.Extra = append(resp.Extra, a) } reqEDNS, respEDNS := &dns.Msg{}, &dns.Msg{} reqEDNS.SetEdns0(math.MaxUint16, true) respEDNS.Answer = append(respEDNS.Answer, resp.Answer...) respEDNS.Extra = append(respEDNS.Extra, resp.Extra...) require.Greater(t, respEDNS.Len(), math.MaxUint16) t.Logf("length is: %v", respEDNS.Len()) if trimmed := trimUDPResponse(reqEDNS, respEDNS, cfg.DNSUDPAnswerLimit); !trimmed { t.Errorf("expected edns to be trimmed: %#v", resp) } require.Greater(t, math.MaxUint16, respEDNS.Len()) t.Logf("length is: %v", respEDNS.Len()) if len(respEDNS.Answer) == 0 || len(respEDNS.Answer) != len(respEDNS.Extra) { t.Errorf("bad edns answer length: %#v", resp) } }) } } func TestDNS_syncExtra(t *testing.T) { resp := &dns.Msg{ Answer: []dns.RR{ // These two are on the same host so the redundant extra // records should get deduplicated. &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Port: 1001, Target: "ip-10-0-1-185.node.dc1.consul.", }, &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Port: 1002, Target: "ip-10-0-1-185.node.dc1.consul.", }, // This one isn't in the Consul domain so it will get a // CNAME and then an A record from the recursor. &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Port: 1003, Target: "demo.consul.io.", }, // This one isn't in the Consul domain and it will get // a CNAME and A record from a recursor that alters the // case of the name. This proves we look up in the index // in a case-insensitive way. &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Port: 1001, Target: "insensitive.consul.io.", }, // This is also a CNAME, but it'll be set up to loop to // make sure we don't crash. &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Port: 1001, Target: "deadly.consul.io.", }, // This is also a CNAME, but it won't have another record. &dns.SRV{ Hdr: dns.RR_Header{ Name: "redis-cache-redis.service.consul.", Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, Port: 1001, Target: "nope.consul.io.", }, }, Extra: []dns.RR{ // These should get deduplicated. &dns.A{ Hdr: dns.RR_Header{ Name: "ip-10-0-1-185.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("10.0.1.185"), }, &dns.A{ Hdr: dns.RR_Header{ Name: "ip-10-0-1-185.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("10.0.1.185"), }, // This is a normal CNAME followed by an A record but we // have flipped the order. The algorithm should emit them // in the opposite order. &dns.A{ Hdr: dns.RR_Header{ Name: "fakeserver.consul.io.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("127.0.0.1"), }, &dns.CNAME{ Hdr: dns.RR_Header{ Name: "demo.consul.io.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "fakeserver.consul.io.", }, // These differ in case to test case insensitivity. &dns.CNAME{ Hdr: dns.RR_Header{ Name: "INSENSITIVE.CONSUL.IO.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "Another.Server.Com.", }, &dns.A{ Hdr: dns.RR_Header{ Name: "another.server.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("127.0.0.1"), }, // This doesn't appear in the answer, so should get // dropped. &dns.A{ Hdr: dns.RR_Header{ Name: "ip-10-0-1-186.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("10.0.1.186"), }, // These two test edge cases with CNAME handling. &dns.CNAME{ Hdr: dns.RR_Header{ Name: "deadly.consul.io.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "deadly.consul.io.", }, &dns.CNAME{ Hdr: dns.RR_Header{ Name: "nope.consul.io.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "notthere.consul.io.", }, }, } index := make(map[string]dns.RR) indexRRs(resp.Extra, index) syncExtra(index, resp) expected := &dns.Msg{ Answer: resp.Answer, Extra: []dns.RR{ &dns.A{ Hdr: dns.RR_Header{ Name: "ip-10-0-1-185.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("10.0.1.185"), }, &dns.CNAME{ Hdr: dns.RR_Header{ Name: "demo.consul.io.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "fakeserver.consul.io.", }, &dns.A{ Hdr: dns.RR_Header{ Name: "fakeserver.consul.io.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("127.0.0.1"), }, &dns.CNAME{ Hdr: dns.RR_Header{ Name: "INSENSITIVE.CONSUL.IO.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "Another.Server.Com.", }, &dns.A{ Hdr: dns.RR_Header{ Name: "another.server.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, }, A: net.ParseIP("127.0.0.1"), }, &dns.CNAME{ Hdr: dns.RR_Header{ Name: "deadly.consul.io.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "deadly.consul.io.", }, &dns.CNAME{ Hdr: dns.RR_Header{ Name: "nope.consul.io.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, }, Target: "notthere.consul.io.", }, }, } if !reflect.DeepEqual(resp, expected) { t.Fatalf("Bad %#v vs. %#v", *resp, *expected) } } func TestDNS_Compression_trimUDPResponse(t *testing.T) { for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { cfg := loadRuntimeConfig(t, `data_dir = "a" bind_addr = "127.0.0.1" node_name = "dummy" `+experimentsHCL) req, m := dns.Msg{}, dns.Msg{} trimUDPResponse(&req, &m, cfg.DNSUDPAnswerLimit) if m.Compress { t.Fatalf("compression should be off") } // The trim function temporarily turns off compression, so we need to // make sure the setting gets restored properly. m.Compress = true trimUDPResponse(&req, &m, cfg.DNSUDPAnswerLimit) if !m.Compress { t.Fatalf("compression should be on") } }) } } func TestDNS_Compression_Query(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Register a node with a service. { args := &structs.RegisterRequest{ Datacenter: "dc1", Node: "foo", Address: "127.0.0.1", Service: &structs.NodeService{ Service: "db", Tags: []string{"primary"}, Port: 12345, }, } var out struct{} if err := a.RPC(context.Background(), "Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } } // Register an equivalent prepared query. var id string { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ Name: "test", Service: structs.ServiceQuery{ Service: "db", }, }, } if err := a.RPC(context.Background(), "PreparedQuery.Apply", args, &id); err != nil { t.Fatalf("err: %v", err) } } // Look up the service directly and via prepared query. questions := []string{ "db.service.consul.", id + ".query.consul.", } for _, question := range questions { m := new(dns.Msg) m.SetQuestion(question, dns.TypeSRV) conn, err := dns.Dial("udp", a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } // Do a manual exchange with compression on (the default). a.DNSDisableCompression(false) if err := conn.WriteMsg(m); err != nil { t.Fatalf("err: %v", err) } p := make([]byte, dns.MaxMsgSize) compressed, err := conn.Read(p) if err != nil { t.Fatalf("err: %v", err) } // Disable compression and try again. a.DNSDisableCompression(true) if err := conn.WriteMsg(m); err != nil { t.Fatalf("err: %v", err) } unc, err := conn.Read(p) if err != nil { t.Fatalf("err: %v", err) } // We can't see the compressed status given the DNS API, so we // just make sure the message is smaller to see if it's // respecting the flag. if compressed == 0 || unc == 0 || compressed >= unc { t.Fatalf("'%s' doesn't look compressed: %d vs. %d", question, compressed, unc) } } }) } } func TestDNS_Compression_Recurse(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } recursor := makeRecursor(t, dns.Msg{ Answer: []dns.RR{dnsA("apple.com", "1.2.3.4")}, }) defer recursor.Shutdown() for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, ` recursors = ["`+recursor.Addr+`"] `+experimentsHCL) defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") m := new(dns.Msg) m.SetQuestion("apple.com.", dns.TypeANY) conn, err := dns.Dial("udp", a.DNSAddr()) if err != nil { t.Fatalf("err: %v", err) } // Do a manual exchange with compression on (the default). if err := conn.WriteMsg(m); err != nil { t.Fatalf("err: %v", err) } p := make([]byte, dns.MaxMsgSize) compressed, err := conn.Read(p) if err != nil { t.Fatalf("err: %v", err) } // Disable compression and try again. a.DNSDisableCompression(true) if err := conn.WriteMsg(m); err != nil { t.Fatalf("err: %v", err) } unc, err := conn.Read(p) if err != nil { t.Fatalf("err: %v", err) } // We can't see the compressed status given the DNS API, so we just make // sure the message is smaller to see if it's respecting the flag. if compressed == 0 || unc == 0 || compressed >= unc { t.Fatalf("doesn't look compressed: %d vs. %d", compressed, unc) } }) } } // TestDNS_V1ConfigReload validates that the dns configuration is saved to the // DNS server when v1 DNS is configured and reload config internal is called. func TestDNS_V1ConfigReload(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } a := NewTestAgent(t, ` recursors = ["8.8.8.8:53"] dns_config = { allow_stale = false max_stale = "20s" node_ttl = "10s" service_ttl = { "my_services*" = "5s" "my_specific_service" = "30s" } enable_truncate = false only_passing = false recursor_strategy = "sequential" recursor_timeout = "15s" disable_compression = false a_record_limit = 1 enable_additional_node_meta_txt = false soa = { refresh = 1 retry = 2 expire = 3 min_ttl = 4 } } experiments = ["v1dns"] `) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") for _, s := range a.dnsServers { server, ok := s.(*DNSServer) require.True(t, ok) cfg := server.config.Load().(*dnsConfig) require.Equal(t, []string{"8.8.8.8:53"}, cfg.Recursors) require.Equal(t, structs.RecursorStrategy("sequential"), cfg.RecursorStrategy) require.False(t, cfg.AllowStale) require.Equal(t, 20*time.Second, cfg.MaxStale) require.Equal(t, 10*time.Second, cfg.NodeTTL) ttl, _ := cfg.GetTTLForService("my_services_1") require.Equal(t, 5*time.Second, ttl) ttl, _ = cfg.GetTTLForService("my_specific_service") require.Equal(t, 30*time.Second, ttl) require.False(t, cfg.EnableTruncate) require.False(t, cfg.OnlyPassing) require.Equal(t, 15*time.Second, cfg.RecursorTimeout) require.False(t, cfg.DisableCompression) require.Equal(t, 1, cfg.ARecordLimit) require.False(t, cfg.NodeMetaTXT) require.Equal(t, uint32(1), cfg.SOAConfig.Refresh) require.Equal(t, uint32(2), cfg.SOAConfig.Retry) require.Equal(t, uint32(3), cfg.SOAConfig.Expire) require.Equal(t, uint32(4), cfg.SOAConfig.Minttl) } newCfg := *a.Config newCfg.DNSRecursors = []string{"1.1.1.1:53"} newCfg.DNSAllowStale = true newCfg.DNSMaxStale = 21 * time.Second newCfg.DNSNodeTTL = 11 * time.Second newCfg.DNSServiceTTL = map[string]time.Duration{ "2_my_services*": 6 * time.Second, "2_my_specific_service": 31 * time.Second, } newCfg.DNSEnableTruncate = true newCfg.DNSOnlyPassing = true newCfg.DNSRecursorStrategy = "random" newCfg.DNSRecursorTimeout = 16 * time.Second newCfg.DNSDisableCompression = true newCfg.DNSARecordLimit = 2 newCfg.DNSNodeMetaTXT = true newCfg.DNSSOA.Refresh = 10 newCfg.DNSSOA.Retry = 20 newCfg.DNSSOA.Expire = 30 newCfg.DNSSOA.Minttl = 40 err := a.reloadConfigInternal(&newCfg) require.NoError(t, err) for _, s := range a.dnsServers { server, ok := s.(*DNSServer) require.True(t, ok) cfg := server.config.Load().(*dnsConfig) require.Equal(t, []string{"1.1.1.1:53"}, cfg.Recursors) require.Equal(t, structs.RecursorStrategy("random"), cfg.RecursorStrategy) require.True(t, cfg.AllowStale) require.Equal(t, 21*time.Second, cfg.MaxStale) require.Equal(t, 11*time.Second, cfg.NodeTTL) ttl, _ := cfg.GetTTLForService("my_services_1") require.Equal(t, time.Duration(0), ttl) ttl, _ = cfg.GetTTLForService("2_my_services_1") require.Equal(t, 6*time.Second, ttl) ttl, _ = cfg.GetTTLForService("my_specific_service") require.Equal(t, time.Duration(0), ttl) ttl, _ = cfg.GetTTLForService("2_my_specific_service") require.Equal(t, 31*time.Second, ttl) require.True(t, cfg.EnableTruncate) require.True(t, cfg.OnlyPassing) require.Equal(t, 16*time.Second, cfg.RecursorTimeout) require.True(t, cfg.DisableCompression) require.Equal(t, 2, cfg.ARecordLimit) require.True(t, cfg.NodeMetaTXT) require.Equal(t, uint32(10), cfg.SOAConfig.Refresh) require.Equal(t, uint32(20), cfg.SOAConfig.Retry) require.Equal(t, uint32(30), cfg.SOAConfig.Expire) require.Equal(t, uint32(40), cfg.SOAConfig.Minttl) } } // TestDNS_V2ConfigReload_WithV1DataFetcher validates that the dns configuration is saved to the // DNS server when v2 DNS is configured with V1 catalog and reload config internal is called. func TestDNS_V2ConfigReload_WithV1DataFetcher(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } a := NewTestAgent(t, ` experiments=["v2dns"] recursors = ["8.8.8.8:53"] dns_config = { allow_stale = false max_stale = "20s" node_ttl = "10s" service_ttl = { "my_services*" = "5s" "my_specific_service" = "30s" } enable_truncate = false only_passing = false recursor_strategy = "sequential" recursor_timeout = "15s" disable_compression = false a_record_limit = 1 enable_additional_node_meta_txt = false soa = { refresh = 1 retry = 2 expire = 3 min_ttl = 4 } } `) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") for _, s := range a.dnsServers { server, ok := s.(*dnsConsul.Server) require.True(t, ok) cfg := server.Router.GetConfig() require.Equal(t, []string{"8.8.8.8:53"}, cfg.Recursors) require.Equal(t, structs.RecursorStrategy("sequential"), cfg.RecursorStrategy) df := a.catalogDataFetcher.(*discovery.V1DataFetcher) dfCfg := df.GetConfig() require.False(t, dfCfg.AllowStale) require.Equal(t, 20*time.Second, dfCfg.MaxStale) require.Equal(t, 10*time.Second, cfg.NodeTTL) ttl, _ := cfg.GetTTLForService("my_services_1") require.Equal(t, 5*time.Second, ttl) ttl, _ = cfg.GetTTLForService("my_specific_service") require.Equal(t, 30*time.Second, ttl) require.False(t, cfg.EnableTruncate) require.False(t, dfCfg.OnlyPassing) require.Equal(t, 15*time.Second, cfg.RecursorTimeout) require.False(t, cfg.DisableCompression) require.Equal(t, 1, cfg.ARecordLimit) require.False(t, cfg.NodeMetaTXT) require.Equal(t, uint32(1), cfg.SOAConfig.Refresh) require.Equal(t, uint32(2), cfg.SOAConfig.Retry) require.Equal(t, uint32(3), cfg.SOAConfig.Expire) require.Equal(t, uint32(4), cfg.SOAConfig.Minttl) } newCfg := *a.Config newCfg.DNSRecursors = []string{"1.1.1.1:53"} newCfg.DNSAllowStale = true newCfg.DNSMaxStale = 21 * time.Second newCfg.DNSNodeTTL = 11 * time.Second newCfg.DNSServiceTTL = map[string]time.Duration{ "2_my_services*": 6 * time.Second, "2_my_specific_service": 31 * time.Second, } newCfg.DNSEnableTruncate = true newCfg.DNSOnlyPassing = true newCfg.DNSRecursorStrategy = "random" newCfg.DNSRecursorTimeout = 16 * time.Second newCfg.DNSDisableCompression = true newCfg.DNSARecordLimit = 2 newCfg.DNSNodeMetaTXT = true newCfg.DNSSOA.Refresh = 10 newCfg.DNSSOA.Retry = 20 newCfg.DNSSOA.Expire = 30 newCfg.DNSSOA.Minttl = 40 err := a.reloadConfigInternal(&newCfg) require.NoError(t, err) for _, s := range a.dnsServers { server, ok := s.(*dnsConsul.Server) require.True(t, ok) cfg := server.Router.GetConfig() require.Equal(t, []string{"1.1.1.1:53"}, cfg.Recursors) require.Equal(t, structs.RecursorStrategy("random"), cfg.RecursorStrategy) df := a.catalogDataFetcher.(*discovery.V1DataFetcher) dfCfg := df.GetConfig() require.True(t, dfCfg.AllowStale) require.Equal(t, 21*time.Second, dfCfg.MaxStale) require.Equal(t, 11*time.Second, cfg.NodeTTL) ttl, _ := cfg.GetTTLForService("my_services_1") require.Equal(t, time.Duration(0), ttl) ttl, _ = cfg.GetTTLForService("2_my_services_1") require.Equal(t, 6*time.Second, ttl) ttl, _ = cfg.GetTTLForService("my_specific_service") require.Equal(t, time.Duration(0), ttl) ttl, _ = cfg.GetTTLForService("2_my_specific_service") require.Equal(t, 31*time.Second, ttl) require.True(t, cfg.EnableTruncate) require.True(t, dfCfg.OnlyPassing) require.Equal(t, 16*time.Second, cfg.RecursorTimeout) require.True(t, cfg.DisableCompression) require.Equal(t, 2, cfg.ARecordLimit) require.True(t, cfg.NodeMetaTXT) require.Equal(t, uint32(10), cfg.SOAConfig.Refresh) require.Equal(t, uint32(20), cfg.SOAConfig.Retry) require.Equal(t, uint32(30), cfg.SOAConfig.Expire) require.Equal(t, uint32(40), cfg.SOAConfig.Minttl) } } // TestDNS_V2ConfigReload_WithV2DataFetcher validates that the dns configuration is saved to the // DNS server when v2 DNS is configured with V1 catalog and reload config internal is called. func TestDNS_V2ConfigReload_WithV2DataFetcher(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } a := NewTestAgent(t, ` experiments=["v2dns", "resource-apis"] recursors = ["8.8.8.8:53"] dns_config = { allow_stale = false max_stale = "20s" node_ttl = "10s" service_ttl = { "my_services*" = "5s" "my_specific_service" = "30s" } enable_truncate = false only_passing = false recursor_strategy = "sequential" recursor_timeout = "15s" disable_compression = false a_record_limit = 1 enable_additional_node_meta_txt = false soa = { refresh = 1 retry = 2 expire = 3 min_ttl = 4 } } `) defer a.Shutdown() // use WaitForRaftLeader with v2 resource apis testrpc.WaitForRaftLeader(t, a.RPC, "dc1") for _, s := range a.dnsServers { server, ok := s.(*dnsConsul.Server) require.True(t, ok) cfg := server.Router.GetConfig() require.Equal(t, []string{"8.8.8.8:53"}, cfg.Recursors) require.Equal(t, structs.RecursorStrategy("sequential"), cfg.RecursorStrategy) df := a.catalogDataFetcher.(*discovery.V2DataFetcher) dfCfg := df.GetConfig() //require.False(t, dfCfg.AllowStale) //require.Equal(t, 20*time.Second, dfCfg.MaxStale) require.Equal(t, 10*time.Second, cfg.NodeTTL) ttl, _ := cfg.GetTTLForService("my_services_1") require.Equal(t, 5*time.Second, ttl) ttl, _ = cfg.GetTTLForService("my_specific_service") require.Equal(t, 30*time.Second, ttl) require.False(t, cfg.EnableTruncate) require.False(t, dfCfg.OnlyPassing) require.Equal(t, 15*time.Second, cfg.RecursorTimeout) require.False(t, cfg.DisableCompression) require.Equal(t, 1, cfg.ARecordLimit) require.False(t, cfg.NodeMetaTXT) require.Equal(t, uint32(1), cfg.SOAConfig.Refresh) require.Equal(t, uint32(2), cfg.SOAConfig.Retry) require.Equal(t, uint32(3), cfg.SOAConfig.Expire) require.Equal(t, uint32(4), cfg.SOAConfig.Minttl) } newCfg := *a.Config newCfg.DNSRecursors = []string{"1.1.1.1:53"} newCfg.DNSAllowStale = true newCfg.DNSMaxStale = 21 * time.Second newCfg.DNSNodeTTL = 11 * time.Second newCfg.DNSServiceTTL = map[string]time.Duration{ "2_my_services*": 6 * time.Second, "2_my_specific_service": 31 * time.Second, } newCfg.DNSEnableTruncate = true newCfg.DNSOnlyPassing = true newCfg.DNSRecursorStrategy = "random" newCfg.DNSRecursorTimeout = 16 * time.Second newCfg.DNSDisableCompression = true newCfg.DNSARecordLimit = 2 newCfg.DNSNodeMetaTXT = true newCfg.DNSSOA.Refresh = 10 newCfg.DNSSOA.Retry = 20 newCfg.DNSSOA.Expire = 30 newCfg.DNSSOA.Minttl = 40 err := a.reloadConfigInternal(&newCfg) require.NoError(t, err) for _, s := range a.dnsServers { server, ok := s.(*dnsConsul.Server) require.True(t, ok) cfg := server.Router.GetConfig() require.Equal(t, []string{"1.1.1.1:53"}, cfg.Recursors) require.Equal(t, structs.RecursorStrategy("random"), cfg.RecursorStrategy) df := a.catalogDataFetcher.(*discovery.V2DataFetcher) dfCfg := df.GetConfig() //require.True(t, dfCfg.AllowStale) //require.Equal(t, 21*time.Second, dfCfg.MaxStale) require.Equal(t, 11*time.Second, cfg.NodeTTL) ttl, _ := cfg.GetTTLForService("my_services_1") require.Equal(t, time.Duration(0), ttl) ttl, _ = cfg.GetTTLForService("2_my_services_1") require.Equal(t, 6*time.Second, ttl) ttl, _ = cfg.GetTTLForService("my_specific_service") require.Equal(t, time.Duration(0), ttl) ttl, _ = cfg.GetTTLForService("2_my_specific_service") require.Equal(t, 31*time.Second, ttl) require.True(t, cfg.EnableTruncate) require.True(t, dfCfg.OnlyPassing) require.Equal(t, 16*time.Second, cfg.RecursorTimeout) require.True(t, cfg.DisableCompression) require.Equal(t, 2, cfg.ARecordLimit) require.True(t, cfg.NodeMetaTXT) require.Equal(t, uint32(10), cfg.SOAConfig.Refresh) require.Equal(t, uint32(20), cfg.SOAConfig.Retry) require.Equal(t, uint32(30), cfg.SOAConfig.Expire) require.Equal(t, uint32(40), cfg.SOAConfig.Minttl) } } func TestDNS_ReloadConfig_DuringQuery(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } for name, experimentsHCL := range getVersionHCL(true) { t.Run(name, func(t *testing.T) { a := NewTestAgent(t, experimentsHCL) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") m := MockPreparedQuery{ executeFn: func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { time.Sleep(100 * time.Millisecond) reply.Nodes = structs.CheckServiceNodes{ { Node: &structs.Node{ ID: "my_node", Address: "127.0.0.1", }, Service: &structs.NodeService{ Address: "127.0.0.1", Port: 8080, }, }, } return nil }, } err := a.registerEndpoint("PreparedQuery", &m) require.NoError(t, err) { m := new(dns.Msg) m.SetQuestion("nope.query.consul.", dns.TypeA) timeout := time.NewTimer(time.Second) res := make(chan *dns.Msg) errs := make(chan error) go func() { c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) if err != nil { errs <- err return } res <- in }() time.Sleep(50 * time.Millisecond) // reload the config halfway through, that should not affect the ongoing query newCfg := *a.Config newCfg.DNSAllowStale = true a.reloadConfigInternal(&newCfg) select { case in := <-res: require.Equal(t, "127.0.0.1", in.Answer[0].(*dns.A).A.String()) case err := <-errs: require.NoError(t, err) case <-timeout.C: require.FailNow(t, "timeout") } } }) } } func TestDNS_ECSNotGlobalError(t *testing.T) { t.Run("wrap nil", func(t *testing.T) { e := ecsNotGlobalError{} require.True(t, errors.Is(e, errECSNotGlobal)) require.False(t, errors.Is(e, fmt.Errorf("some other error"))) require.Equal(t, nil, errors.Unwrap(e)) }) t.Run("wrap some error", func(t *testing.T) { e := ecsNotGlobalError{error: errNameNotFound} require.True(t, errors.Is(e, errECSNotGlobal)) require.False(t, errors.Is(e, fmt.Errorf("some other error"))) require.Equal(t, errNameNotFound, errors.Unwrap(e)) }) } // perfectlyRandomChoices assigns exactly the provided fraction of size items a // true value, and then presents a random permutation of those boolean values. func perfectlyRandomChoices(size int, frac float64) []bool { out := make([]bool, size) max := int(float64(size) * frac) for i := 0; i < max; i++ { out[i] = true } rand.Shuffle(size, func(i, j int) { out[i], out[j] = out[j], out[i] }) return out } func TestDNS_PerfectlyRandomChoices(t *testing.T) { count := func(got []bool) int { var x int for _, v := range got { if v { x++ } } return x } type testcase struct { size int frac float64 expect int } run := func(t *testing.T, tc testcase) { got := perfectlyRandomChoices(tc.size, tc.frac) require.Equal(t, tc.expect, count(got)) } cases := []testcase{ // 100% {0, 1, 0}, {1, 1, 1}, {2, 1, 2}, {3, 1, 3}, {5, 1, 5}, // 50% {0, 0.5, 0}, {1, 0.5, 0}, {2, 0.5, 1}, {3, 0.5, 1}, {5, 0.5, 2}, // 10% {0, 0.1, 0}, {1, 0.1, 0}, {2, 0.1, 0}, {3, 0.1, 0}, {5, 0.1, 0}, {10, 0.1, 1}, {11, 0.1, 1}, {15, 0.1, 1}, } for _, tc := range cases { t.Run(fmt.Sprintf("size=%d frac=%g", tc.size, tc.frac), func(t *testing.T) { run(t, tc) }) } } type testCaseParseLocality struct { name string labels []string defaultEntMeta acl.EnterpriseMeta enterpriseDNSConfig enterpriseDNSConfig expectedResult queryLocality expectedOK bool } func TestDNS_ParseLocality(t *testing.T) { testCases := getTestCasesParseLocality() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { d := &DNSServer{ defaultEnterpriseMeta: tc.defaultEntMeta, } actualResult, actualOK := d.parseLocality(tc.labels, &dnsConfig{ enterpriseDNSConfig: tc.enterpriseDNSConfig, }) require.Equal(t, tc.expectedOK, actualOK) require.Equal(t, tc.expectedResult, actualResult) }) } } func TestDNS_EffectiveDatacenter(t *testing.T) { type testCase struct { name string queryLocality queryLocality defaultDC string expected string } testCases := []testCase{ { name: "return datacenter first", queryLocality: queryLocality{ datacenter: "test-dc", peerOrDatacenter: "test-peer", }, defaultDC: "default-dc", expected: "test-dc", }, { name: "return PeerOrDatacenter second", queryLocality: queryLocality{ peerOrDatacenter: "test-peer", }, defaultDC: "default-dc", expected: "test-peer", }, { name: "return defaultDC as fallback", queryLocality: queryLocality{}, defaultDC: "default-dc", expected: "default-dc", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := tc.queryLocality.effectiveDatacenter(tc.defaultDC) require.Equal(t, tc.expected, got) }) } }