From 097c768bd3b70c5bbeef8ffee2125c312b307715 Mon Sep 17 00:00:00 2001 From: Juliano Martinez Date: Wed, 22 Jan 2025 11:19:05 +0100 Subject: [PATCH] adds handling for RFC 9156 --- .changelog/22095.txt | 2 + agent/dns.go | 12 ++++- agent/dns_test.go | 106 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 .changelog/22095.txt diff --git a/.changelog/22095.txt b/.changelog/22095.txt new file mode 100644 index 000000000000..809333eb7d78 --- /dev/null +++ b/.changelog/22095.txt @@ -0,0 +1,2 @@ +release-note:improvement +dns: qname minimization handling based on rfc9156 diff --git a/agent/dns.go b/agent/dns.go index dd34b3b8bda7..cc88aea6c66f 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -8,7 +8,6 @@ import ( "encoding/hex" "errors" "fmt" - agentdns "github.com/hashicorp/consul/agent/dns" "math" "net" "regexp" @@ -16,6 +15,8 @@ import ( "sync/atomic" "time" + agentdns "github.com/hashicorp/consul/agent/dns" + "github.com/armon/go-metrics" "github.com/armon/go-radix" "github.com/hashicorp/go-hclog" @@ -801,6 +802,15 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, cfg *dnsRe } invalid := func() error { + // Handle potential QNAME minimization queries + // Resolvers using QNAME minimization will send queries with partial names + // RFC 9156: Allow queries for partial domain components when they look like QNAME minimization + // eg. "service.dc.consul" or "service.consul" + if len(queryParts) == 0 && (queryKind != "") { + // Return empty non-referral response (RFC 9156 Section 3) + resp.Answer = []dns.RR{} + return nil + } d.logger.Warn("QName invalid", "qname", qName) return errNameNotFound } diff --git a/agent/dns_test.go b/agent/dns_test.go index 56c549cb6bc3..2fb6a0e8a270 100644 --- a/agent/dns_test.go +++ b/agent/dns_test.go @@ -2231,9 +2231,6 @@ func TestDNS_InvalidQueries(t *testing.T) { // 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.", @@ -3439,3 +3436,106 @@ func TestDNS_EffectiveDatacenter(t *testing.T) { }) } } + +func TestDNS_QNAMEMinimization(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := NewTestAgent(t, "") + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + // Register a node with a service to ensure service lookups have a valid target + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "api", + Tags: []string{"a"}, + Port: 5000, + }, + } + + var out struct{} + require.NoError(t, a.RPC(context.Background(), "Catalog.Register", args, &out)) + } + + // Test cases for QNAME minimization partial queries - these should return empty success + partialQueries := []struct { + name string + query string + queryType uint16 + }{ + {"service-component", "service.consul.", dns.TypeA}, + {"service-component-any", "service.consul.", dns.TypeANY}, + {"node-component", "node.consul.", dns.TypeA}, + {"node-component-any", "node.consul.", dns.TypeANY}, + {"addr-component", "addr.consul.", dns.TypeA}, + {"addr-component-any", "addr.consul.", dns.TypeANY}, + {"query-component", "query.consul.", dns.TypeA}, + {"query-component-any", "query.consul.", dns.TypeANY}, + {"service-txt-type", "service.consul.", dns.TypeTXT}, + } + + for _, tc := range partialQueries { + t.Run(tc.name, func(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion(tc.query, tc.queryType) + + c := new(dns.Client) + in, _, err := c.Exchange(m, a.DNSAddr()) + require.NoError(t, err) + require.Equal(t, dns.RcodeSuccess, in.Rcode) + require.Empty(t, in.Answer) + }) + } + + // Test cases for actual service/node lookups - these should return real records + fullQueries := []struct { + name string + query string + queryType uint16 + checkFn func(*testing.T, *dns.Msg) + }{ + { + "service-lookup", + "api.service.consul.", + dns.TypeA, + func(t *testing.T, in *dns.Msg) { + require.Len(t, in.Answer, 1) + aRec, ok := in.Answer[0].(*dns.A) + require.True(t, ok) + require.Equal(t, net.ParseIP("127.0.0.1").String(), aRec.A.String()) + }, + }, + { + "node-lookup", + "foo.node.consul.", + dns.TypeA, + func(t *testing.T, in *dns.Msg) { + require.Len(t, in.Answer, 1) + aRec, ok := in.Answer[0].(*dns.A) + require.True(t, ok) + require.Equal(t, net.ParseIP("127.0.0.1").String(), aRec.A.String()) + }, + }, + } + + for _, tc := range fullQueries { + t.Run(tc.name, func(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion(tc.query, tc.queryType) + + c := new(dns.Client) + in, _, err := c.Exchange(m, a.DNSAddr()) + require.NoError(t, err) + require.Equal(t, dns.RcodeSuccess, in.Rcode) + tc.checkFn(t, in) + }) + } +}