diff --git a/.changelog/14465.txt b/.changelog/14465.txt new file mode 100644 index 0000000000..8fbdf14e6a --- /dev/null +++ b/.changelog/14465.txt @@ -0,0 +1,3 @@ +```release-note:improvement +dns: support RFC 2782 SRV lookups for prepared queries using format `_._tcp.query[.].`. +``` diff --git a/agent/dns.go b/agent/dns.go index d458928e80..b671cf5492 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -908,10 +908,11 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi return d.nodeLookup(cfg, lookup, req, resp) case "query": + n := len(queryParts) datacenter := d.agent.config.Datacenter // ensure we have a query name - if len(queryParts) < 1 { + if n < 1 { return invalid() } @@ -919,8 +920,23 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi return invalid() } - // Allow a "." in the query name, just join all the parts. - query := strings.Join(queryParts, ".") + query := "" + + // If the first and last DNS query parts begin with _, this is an RFC 2782 style SRV lookup. + // This allows for prepared query names to include "." (for backwards compatibility). + // Otherwise, this is a standard prepared query lookup. + if n >= 2 && strings.HasPrefix(queryParts[0], "_") && strings.HasPrefix(queryParts[n-1], "_") { + // The last DNS query part is the protocol field (ignored). + // All prior parts are the prepared query name or ID. + query = strings.Join(queryParts[:n-1], ".") + + // Strip leading underscore + query = query[1:] + } else { + // Allow a "." in the query name, just join all the parts. + query = strings.Join(queryParts, ".") + } + err := d.preparedQueryLookup(cfg, datacenter, query, remoteAddr, req, resp, maxRecursionLevel) return ecsNotGlobalError{error: err} diff --git a/agent/dns_test.go b/agent/dns_test.go index 2f2499a2ef..189859b9d0 100644 --- a/agent/dns_test.go +++ b/agent/dns_test.go @@ -2743,13 +2743,16 @@ func TestDNS_ServiceLookup_ServiceAddress_SRV(t *testing.T) { } // Register an equivalent prepared query. + // Specify prepared query name containing "." to test + // since that is technically supported (though atypical). var id string + preparedQueryName := "query.name.with.dots" { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ - Name: "test", + Name: preparedQueryName, Service: structs.ServiceQuery{ Service: "db", }, @@ -2764,6 +2767,9 @@ func TestDNS_ServiceLookup_ServiceAddress_SRV(t *testing.T) { questions := []string{ "db.service.consul.", id + ".query.consul.", + preparedQueryName + ".query.consul.", + fmt.Sprintf("_%s._tcp.query.consul.", id), + fmt.Sprintf("_%s._tcp.query.consul.", preparedQueryName), } for _, question := range questions { m := new(dns.Msg) diff --git a/website/content/docs/discovery/dns.mdx b/website/content/docs/discovery/dns.mdx index 29f3e9645b..93074df540 100644 --- a/website/content/docs/discovery/dns.mdx +++ b/website/content/docs/discovery/dns.mdx @@ -396,16 +396,24 @@ you can use the following query formats specify namespace but not partition: ### Prepared Query Lookups -The format of a prepared query lookup is: +The following formats are valid for prepared query lookups: -```text -.query[.]. -``` +- Standard lookup + + ```text + .query[.]. + ``` + +- [RFC 2782](https://tools.ietf.org/html/rfc2782) SRV lookup + + ```text + _._tcp.query[.]. + ``` The `datacenter` is optional, and if not provided, the datacenter of this Consul agent is assumed. -The `query or name` is the ID or given name of an existing +The `query name or id` is the given name or ID of an existing [Prepared Query](/api-docs/query). These behave like standard service queries but provide a much richer set of features, such as filtering by multiple tags and automatically failing over to look for services in remote datacenters if