diff --git a/.changelog/11348.txt b/.changelog/11348.txt new file mode 100644 index 000000000..afcb9b2ca --- /dev/null +++ b/.changelog/11348.txt @@ -0,0 +1,3 @@ +```release-note:bug +dns: Fixed an issue where on DNS requests made with .alt_domain response was returned as .domain +``` \ No newline at end of file diff --git a/agent/dns.go b/agent/dns.go index b95dbefec..47f2edae5 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -348,6 +348,20 @@ func serviceIngressDNSName(service, datacenter, domain string, entMeta *structs. return serviceCanonicalDNSName(service, "ingress", datacenter, domain, entMeta) } +// getResponseDomain returns alt-domain if it is configured and request is made with alt-domain, +// respects DNS case insensitivity +func (d *DNSServer) getResponseDomain(questionName string) string { + labels := dns.SplitDomainName(questionName) + domain := d.domain + for i := len(labels) - 1; i >= 0; i-- { + currentSuffix := strings.Join(labels[i:], ".") + "." + if strings.EqualFold(currentSuffix, d.domain) || strings.EqualFold(currentSuffix, d.altDomain) { + domain = currentSuffix + } + } + return domain +} + // handlePtr is used to handle "reverse" DNS queries func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { q := req.Question[0] @@ -485,14 +499,14 @@ func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) { switch req.Question[0].Qtype { case dns.TypeSOA: - ns, glue := d.nameservers(cfg, maxRecursionLevelDefault) + ns, glue := d.nameservers(req.Question[0].Name, cfg, maxRecursionLevelDefault) m.Answer = append(m.Answer, d.soa(cfg, q.Name)) m.Ns = append(m.Ns, ns...) m.Extra = append(m.Extra, glue...) m.SetRcode(req, dns.RcodeSuccess) case dns.TypeNS: - ns, glue := d.nameservers(cfg, maxRecursionLevelDefault) + ns, glue := d.nameservers(req.Question[0].Name, cfg, maxRecursionLevelDefault) m.Answer = ns m.Extra = glue m.SetRcode(req, dns.RcodeSuccess) @@ -550,7 +564,7 @@ func (d *DNSServer) addSOA(cfg *dnsConfig, msg *dns.Msg, questionName string) { // nameservers returns the names and ip addresses of up to three random servers // in the current cluster which serve as authoritative name servers for zone. -func (d *DNSServer) nameservers(cfg *dnsConfig, maxRecursionLevel int) (ns []dns.RR, extra []dns.RR) { +func (d *DNSServer) nameservers(questionName string, cfg *dnsConfig, maxRecursionLevel int) (ns []dns.RR, extra []dns.RR) { out, err := d.lookupServiceNodes(cfg, serviceLookup{ Datacenter: d.agent.config.Datacenter, Service: structs.ConsulServiceName, @@ -578,14 +592,14 @@ func (d *DNSServer) nameservers(cfg *dnsConfig, maxRecursionLevel int) (ns []dns d.logger.Warn("Skipping invalid node for NS records", "node", name) continue } - - fqdn := name + ".node." + dc + "." + d.domain + respDomain := d.getResponseDomain(questionName) + fqdn := name + ".node." + dc + "." + respDomain fqdn = dns.Fqdn(strings.ToLower(fqdn)) // NS record nsrr := &dns.NS{ Hdr: dns.RR_Header{ - Name: d.domain, + Name: respDomain, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: uint32(cfg.NodeTTL / time.Second), @@ -662,6 +676,9 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi // have to deref to clone it so we don't modify (start from the agent's defaults) var entMeta = d.defaultEnterpriseMeta + // Choose correct response domain + respDomain := d.getResponseDomain(req.Question[0].Name) + // Get the QName without the domain suffix qName := strings.ToLower(dns.Fqdn(req.Question[0].Name)) qName = d.trimDomain(qName) @@ -833,7 +850,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi //check if the query type is A for IPv4 or ANY aRecord := &dns.A{ Hdr: dns.RR_Header{ - Name: qName + d.domain, + Name: qName + respDomain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: uint32(cfg.NodeTTL / time.Second), @@ -854,7 +871,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi //check if the query type is AAAA for IPv6 or ANY aaaaRecord := &dns.AAAA{ Hdr: dns.RR_Header{ - Name: qName + d.domain, + Name: qName + respDomain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: uint32(cfg.NodeTTL / time.Second), @@ -1535,13 +1552,14 @@ func findWeight(node structs.CheckServiceNode) int { } } -func (d *DNSServer) encodeIPAsFqdn(dc string, ip net.IP) string { +func (d *DNSServer) encodeIPAsFqdn(questionName string, dc string, ip net.IP) string { ipv4 := ip.To4() + respDomain := d.getResponseDomain(questionName) if ipv4 != nil { ipStr := hex.EncodeToString(ip) - return fmt.Sprintf("%s.addr.%s.%s", ipStr[len(ipStr)-(net.IPv4len*2):], dc, d.domain) + return fmt.Sprintf("%s.addr.%s.%s", ipStr[len(ipStr)-(net.IPv4len*2):], dc, respDomain) } else { - return fmt.Sprintf("%s.addr.%s.%s", hex.EncodeToString(ip), dc, d.domain) + return fmt.Sprintf("%s.addr.%s.%s", hex.EncodeToString(ip), dc, respDomain) } } @@ -1623,13 +1641,14 @@ func (d *DNSServer) makeRecordFromNode(node *structs.Node, qType uint16, qName s // Otherwise it will return a IN A record func (d *DNSServer) makeRecordFromServiceNode(dc string, serviceNode structs.CheckServiceNode, addr net.IP, req *dns.Msg, ttl time.Duration) ([]dns.RR, []dns.RR) { q := req.Question[0] + respDomain := d.getResponseDomain(q.Name) + ipRecord := makeARecord(q.Qtype, addr, ttl) if ipRecord == nil { return nil, nil } - if q.Qtype == dns.TypeSRV { - nodeFQDN := fmt.Sprintf("%s.node.%s.%s", serviceNode.Node.Node, dc, d.domain) + nodeFQDN := fmt.Sprintf("%s.node.%s.%s", serviceNode.Node.Node, dc, respDomain) answers := []dns.RR{ &dns.SRV{ Hdr: dns.RR_Header{ @@ -1664,7 +1683,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs } if q.Qtype == dns.TypeSRV { - ipFQDN := d.encodeIPAsFqdn(dc, addr) + ipFQDN := d.encodeIPAsFqdn(q.Name, dc, addr) answers := []dns.RR{ &dns.SRV{ Hdr: dns.RR_Header{ @@ -1833,11 +1852,12 @@ func (d *DNSServer) serviceSRVRecords(cfg *dnsConfig, dc string, nodes structs.C answers, extra := d.nodeServiceRecords(dc, node, req, ttl, cfg, maxRecursionLevel) + respDomain := d.getResponseDomain(req.Question[0].Name) resp.Answer = append(resp.Answer, answers...) resp.Extra = append(resp.Extra, extra...) if cfg.NodeMetaTXT { - resp.Extra = append(resp.Extra, d.generateMeta(fmt.Sprintf("%s.node.%s.%s", node.Node.Node, dc, d.domain), node.Node, ttl)...) + resp.Extra = append(resp.Extra, d.generateMeta(fmt.Sprintf("%s.node.%s.%s", node.Node.Node, dc, respDomain), node.Node, ttl)...) } } } diff --git a/agent/dns_test.go b/agent/dns_test.go index 32fdef687..80b7a93e9 100644 --- a/agent/dns_test.go +++ b/agent/dns_test.go @@ -2128,6 +2128,58 @@ func TestDNS_NSRecords(t *testing.T) { require.Equal(t, wantExtra, in.Extra, "extra") } +func TestDNS_AltDomain_NSRecords(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, ` + domain = "CONSUL." + node_name = "server1" + alt_domain = "test-domain." + `) + 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") @@ -2169,6 +2221,59 @@ func TestDNS_NSRecords_IPV6(t *testing.T) { } +func TestDNS_AltDomain_NSRecords_IPV6(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, ` + domain = "CONSUL." + node_name = "server1" + advertise_addr = "::1" + alt_domain = "test-domain." + `) + 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_ExternalServiceToConsulCNAMENestedLookup(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -2397,6 +2502,110 @@ func TestDNS_ServiceLookup_ServiceAddress_A(t *testing.T) { } } +func TestDNS_AltDomain_ServiceLookup_ServiceAddress_A(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, ` + alt_domain = "test-domain" + `) + 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"}, + Address: "127.0.0.2", + Port: 12345, + }, + } + + var out struct{} + if err := a.RPC("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("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Look up the service directly and via prepared query. + questions := []struct { + ask string + wantDomain string + }{ + {"db.service.consul.", "consul."}, + {id + ".query.consul.", "consul."}, + {"db.service.test-domain.", "test-domain."}, + {id + ".query.test-domain.", "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 srvRec.Target != "7f000002.addr.dc1."+question.wantDomain { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + aRec, ok := in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Name != "7f000002.addr.dc1."+question.wantDomain { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.A.String() != "127.0.0.2" { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + } +} + func TestDNS_ServiceLookup_ServiceAddress_SRV(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -2605,6 +2814,110 @@ func TestDNS_ServiceLookup_ServiceAddressIPV6(t *testing.T) { } } +func TestDNS_AltDomain_ServiceLookup_ServiceAddressIPV6(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, ` + alt_domain = "test-domain" + `) + 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"}, + Address: "2607:20:4005:808::200e", + Port: 12345, + }, + } + + var out struct{} + if err := a.RPC("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("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Look up the service directly and via prepared query. + questions := []struct { + ask string + want string + }{ + {"db.service.consul.", "2607002040050808000000000000200e.addr.dc1.consul."}, + {"db.service.test-domain.", "2607002040050808000000000000200e.addr.dc1.test-domain."}, + {id + ".query.consul.", "2607002040050808000000000000200e.addr.dc1.consul."}, + {id + ".query.test-domain.", "2607002040050808000000000000200e.addr.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 srvRec.Target != question.want { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + aRec, ok := in.Extra[0].(*dns.AAAA) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Name != question.want { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.AAAA.String() != "2607:20:4005:808::200e" { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + } +} + func TestDNS_ServiceLookup_WanTranslation(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -6462,6 +6775,9 @@ func TestDNS_AltDomains_Service(t *testing.T) { Tags: []string{"primary"}, Port: 12345, }, + NodeMeta: map[string]string{ + "key": "value", + }, } var out struct{} @@ -6470,16 +6786,19 @@ func TestDNS_AltDomains_Service(t *testing.T) { } } - questions := []string{ - "db.service.consul.", - "db.service.test-domain.", - "db.service.dc1.consul.", - "db.service.dc1.test-domain.", + 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, dns.TypeSRV) + m.SetQuestion(question.ask, dns.TypeSRV) c := new(dns.Client) in, _, err := c.Exchange(m, a.DNSAddr()) @@ -6498,20 +6817,33 @@ func TestDNS_AltDomains_Service(t *testing.T) { if srvRec.Port != 12345 { t.Fatalf("Bad: %#v", srvRec) } - if srvRec.Target != "test-node.node.dc1.consul." { - 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 aRec.Hdr.Name != "test-node.node.dc1.consul." { - 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]) + } } } diff --git a/website/content/docs/agent/options.mdx b/website/content/docs/agent/options.mdx index 9703605a0..570d7dcd1 100644 --- a/website/content/docs/agent/options.mdx +++ b/website/content/docs/agent/options.mdx @@ -213,6 +213,9 @@ The options below are all specified on the command-line. DNS queries in an alternate domain, in addition to the primary domain. If unset, no alternate domain is used. + In Consul 1.10.4 and later, Consul DNS responses will use the same domain as in the query (`-domain` or `-alt-domain`) where applicable. + PTR query responses will always use `-domain`, since the desired domain cannot be included in the query. + - `-enable-script-checks` ((#\_enable_script_checks)) This controls whether [health checks that execute scripts](/docs/agent/checks) are enabled on this agent, and defaults to `false` so operators must opt-in to allowing these. This @@ -824,6 +827,8 @@ Valid time units are 'ns', 'us' (or 'µs'), 'ms', 's', 'm', 'h'." removed from the cluster. This may only be set on client agents and if unset then other nodes will use the main `reconnect_timeout` setting when determining when this node may be removed from the cluster. +- `alt_domain` Equivalent to the [`-alt-domain` command-line flag](#_alt_domain) + - `serf_lan` ((#serf_lan_bind)) Equivalent to the [`-serf-lan-bind` command-line flag](#_serf_lan_bind). This is an IP address, not to be confused with [`ports.serf_lan`](#serf_lan_port). diff --git a/website/content/docs/discovery/dns.mdx b/website/content/docs/discovery/dns.mdx index 85e97168d..a13cd4c22 100644 --- a/website/content/docs/discovery/dns.mdx +++ b/website/content/docs/discovery/dns.mdx @@ -21,9 +21,9 @@ are located in the `us-east-1` datacenter, and have no failing health checks. It's that simple! There are a number of configuration options that are important for the DNS interface, -specifically [`client_addr`](/docs/agent/options#client_addr), -[`ports.dns`](/docs/agent/options#dns_port), [`recursors`](/docs/agent/options#recursors), -[`domain`](/docs/agent/options#domain), and [`dns_config`](/docs/agent/options#dns_config). +specifically [`client_addr`](/docs/agent/options#client_addr),[`ports.dns`](/docs/agent/options#dns_port), +[`recursors`](/docs/agent/options#recursors),[`domain`](/docs/agent/options#domain), +[`alt_domain`](/docs/agent/options#alt_domain), and [`dns_config`](/docs/agent/options#dns_config). By default, Consul will listen on 127.0.0.1:8600 for DNS queries in the `consul.` domain, without support for further DNS recursion. Please consult the [documentation on configuration options](/docs/agent/options),