// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package dns import ( "encoding/hex" "errors" "fmt" "github.com/hashicorp/consul/acl" "net" "regexp" "strings" "sync/atomic" "time" "github.com/armon/go-radix" "github.com/miekg/dns" "github.com/hashicorp/go-hclog" "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/discovery" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/internal/dnsutil" "github.com/hashicorp/consul/logging" ) const ( addrLabel = "addr" arpaDomain = "arpa." arpaLabel = "arpa" suffixFailover = "failover." suffixNoFailover = "no-failover." maxRecursionLevelDefault = 3 // This field comes from the V1 DNS server and affects V1 catalog lookups maxRecurseRecords = 5 ) var ( errInvalidQuestion = fmt.Errorf("invalid question") errNameNotFound = fmt.Errorf("name not found") errNotImplemented = fmt.Errorf("not implemented") errRecursionFailed = fmt.Errorf("recursion failed") trailingSpacesRE = regexp.MustCompile(" +$") ) // TODO (v2-dns): metrics // Context is used augment a DNS message with Consul-specific metadata. type Context struct { Token string DefaultPartition string } // RouterDynamicConfig is the dynamic configuration that can be hot-reloaded type RouterDynamicConfig struct { ARecordLimit int DisableCompression bool EnableDefaultFailover bool // TODO (v2-dns): plumbing required for this new V2 setting. This is the agent configured default EnableTruncate bool NodeMetaTXT bool NodeTTL time.Duration Recursors []string RecursorTimeout time.Duration RecursorStrategy structs.RecursorStrategy SOAConfig SOAConfig // TTLRadix sets service TTLs by prefix, eg: "database-*" TTLRadix *radix.Tree // TTLStrict sets TTLs to service by full name match. It Has higher priority than TTLRadix TTLStrict map[string]time.Duration UDPAnswerLimit int } type SOAConfig struct { Refresh uint32 // 3600 by default Retry uint32 // 600 Expire uint32 // 86400 Minttl uint32 // 0 } // DiscoveryQueryProcessor is an interface that can be used by any consumer requesting Service Discovery results. // This could be attached to a gRPC endpoint in the future in addition to DNS. // Making this an interface means testing the router with a mock is trivial. type DiscoveryQueryProcessor interface { QueryByName(*discovery.Query, discovery.Context) ([]*discovery.Result, error) QueryByIP(net.IP, discovery.Context) ([]*discovery.Result, error) } // dnsRecursor is an interface that can be used to mock calls to external DNS servers for unit testing. // //go:generate mockery --name dnsRecursor --inpackage type dnsRecursor interface { handle(req *dns.Msg, cfgCtx *RouterDynamicConfig, remoteAddress net.Addr) (*dns.Msg, error) } // Router replaces miekg/dns.ServeMux with a simpler router that only checks for the 2-3 valid domains // that Consul supports and forwards to a single DiscoveryQueryProcessor handler. If there is no match, it will recurse. type Router struct { processor DiscoveryQueryProcessor recursor dnsRecursor domain string altDomain string datacenter string logger hclog.Logger tokenFunc func() string // dynamicConfig stores the config as an atomic value (for hot-reloading). // It is always of type *RouterDynamicConfig dynamicConfig atomic.Value } var _ = dns.Handler(&Router{}) var _ = DNSRouter(&Router{}) func NewRouter(cfg Config) (*Router, error) { // Make sure domains are FQDN, make them case-insensitive for DNSRequestRouter domain := dns.CanonicalName(cfg.AgentConfig.DNSDomain) altDomain := dns.CanonicalName(cfg.AgentConfig.DNSAltDomain) // TODO (v2-dns): need to figure out tenancy information here in a way that work for V2 and V1 logger := cfg.Logger.Named(logging.DNS) router := &Router{ processor: cfg.Processor, recursor: newRecursor(logger), domain: domain, altDomain: altDomain, datacenter: cfg.AgentConfig.Datacenter, logger: logger, tokenFunc: cfg.TokenFunc, } if err := router.ReloadConfig(cfg.AgentConfig); err != nil { return nil, err } return router, nil } // HandleRequest is used to process an individual DNS request. It returns a message in success or fail cases. func (r *Router) HandleRequest(req *dns.Msg, reqCtx Context, remoteAddress net.Addr) *dns.Msg { return r.handleRequestRecursively(req, reqCtx, remoteAddress, maxRecursionLevelDefault) } // getErrorFromECSNotGlobalError returns the underlying error from an ECSNotGlobalError, if it exists. func getErrorFromECSNotGlobalError(err error) error { if errors.Is(err, discovery.ErrECSNotGlobal) { return err.(discovery.ECSNotGlobalError).Unwrap() } return err } // handleRequestRecursively is used to process an individual DNS request. It will recurse as needed // a maximum number of times and returns a message in success or fail cases. func (r *Router) handleRequestRecursively(req *dns.Msg, reqCtx Context, remoteAddress net.Addr, maxRecursionLevel int) *dns.Msg { configCtx := r.dynamicConfig.Load().(*RouterDynamicConfig) err := validateAndNormalizeRequest(req) if err != nil { r.logger.Error("error parsing DNS query", "error", err) if errors.Is(err, errInvalidQuestion) { return createRefusedResponse(req) } return createServerFailureResponse(req, configCtx, false) } responseDomain, needRecurse := r.parseDomain(req.Question[0].Name) if needRecurse && !canRecurse(configCtx) { // This is the same error as an unmatched domain return createRefusedResponse(req) } if needRecurse { // This assumes `canRecurse(configCtx)` is true above resp, err := r.recursor.handle(req, configCtx, remoteAddress) if err != nil && !errors.Is(err, errRecursionFailed) { r.logger.Error("unhandled error recursing DNS query", "error", err) } if err != nil { return createServerFailureResponse(req, configCtx, true) } return resp } // Need to pass the question name to properly support recursion and the // trimming of the domain suffixes. qName := dns.CanonicalName(req.Question[0].Name) if maxRecursionLevel < maxRecursionLevelDefault { // Get the QName without the domain suffix qName = r.trimDomain(qName) } reqType := parseRequestType(req) results, query, err := r.getQueryResults(req, reqCtx, reqType, qName, remoteAddress) // incase of the wrapped ECSNotGlobalError, extract the error from it. isECSGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) err = getErrorFromECSNotGlobalError(err) if err != nil { switch { case errors.Is(err, errInvalidQuestion): r.logger.Error("invalid question", "name", qName) ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeNameError, ecsGlobal) case errors.Is(err, errNameNotFound): r.logger.Error("name not found", "name", qName) ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeNameError, ecsGlobal) case errors.Is(err, errNotImplemented): r.logger.Error("query not implemented", "name", qName, "type", dns.Type(req.Question[0].Qtype).String()) ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeNotImplemented, ecsGlobal) case errors.Is(err, discovery.ErrNotSupported): r.logger.Debug("query name syntax not supported", "name", req.Question[0].Name) ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeNameError, ecsGlobal) case errors.Is(err, discovery.ErrNotFound): r.logger.Debug("query name not found", "name", req.Question[0].Name) ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeNameError, ecsGlobal) case errors.Is(err, discovery.ErrNoData): r.logger.Debug("no data available", "name", qName) ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal) return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeSuccess, ecsGlobal) default: r.logger.Error("error processing discovery query", "error", err) return createServerFailureResponse(req, configCtx, canRecurse(configCtx)) } } // This needs the question information because it affects the serialization format. // e.g., the Consul service has the same "results" for both NS and A/AAAA queries, but the serialization differs. resp, err := r.serializeQueryResults(req, reqCtx, query, results, configCtx, responseDomain, remoteAddress, maxRecursionLevel) if err != nil { r.logger.Error("error serializing DNS results", "error", err) return createServerFailureResponse(req, configCtx, false) } // Switch to TCP if the client is network := "udp" if _, ok := remoteAddress.(*net.TCPAddr); ok { network = "tcp" } trimDNSResponse(configCtx, network, req, resp, r.logger) setEDNS(req, resp, isECSGlobal) return resp } // trimDomain trims the domain from the question name. func (r *Router) trimDomain(questionName string) string { longer := r.domain shorter := r.altDomain if len(shorter) > len(longer) { longer, shorter = shorter, longer } if strings.HasSuffix(questionName, "."+strings.TrimLeft(longer, ".")) { return strings.TrimSuffix(questionName, longer) } return strings.TrimSuffix(questionName, shorter) } // getTTLForResult returns the TTL for a given result. func getTTLForResult(name string, query *discovery.Query, cfg *RouterDynamicConfig) uint32 { // In the case we are not making a discovery query, such as addr. or arpa. lookups, // use the node TTL by convention if query == nil { return uint32(cfg.NodeTTL / time.Second) } switch query.QueryType { // TODO (v2-dns): currently have to do this related to the results type being changed to node whe // the v1 data fetcher encounters a blank service address and uses the node address instead. // we will revisiting this when look at modifying the discovery result struct to // possibly include additional metadata like the node address. case discovery.QueryTypeWorkload: // TODO (v2-dns): we need to discuss what we want to do for workload TTLs return 0 case discovery.QueryTypeService: ttl, ok := cfg.getTTLForService(name) if ok { return uint32(ttl / time.Second) } fallthrough default: return uint32(cfg.NodeTTL / time.Second) } } // getQueryResults returns a discovery.Result from a DNS message. func (r *Router) getQueryResults(req *dns.Msg, reqCtx Context, reqType requestType, qName string, remoteAddress net.Addr) ([]*discovery.Result, *discovery.Query, error) { switch reqType { case requestTypeConsul: // This is a special case of discovery.QueryByName where we know that we need to query the consul service // regardless of the question name. query := &discovery.Query{ QueryType: discovery.QueryTypeService, QueryPayload: discovery.QueryPayload{ Name: structs.ConsulServiceName, Tenancy: discovery.QueryTenancy{ // We specify the partition here so that in the case we are a client agent in a non-default partition. // We don't want the query processors default partition to be used. // This is a small hack because for V1 CE, this is not the correct default partition name, but we // need to add something to disambiguate the empty field. Partition: acl.DefaultPartitionName, //NOTE: note this won't work if we ever have V2 client agents }, Limit: 3, }, } results, err := r.processor.QueryByName(query, discovery.Context{Token: reqCtx.Token}) return results, query, err case requestTypeName: query, err := buildQueryFromDNSMessage(req, reqCtx, r.domain, r.altDomain, remoteAddress) if err != nil { r.logger.Error("error building discovery query from DNS request", "error", err) return nil, query, err } results, err := r.processor.QueryByName(query, discovery.Context{Token: reqCtx.Token}) if getErrorFromECSNotGlobalError(err) != nil { r.logger.Error("error processing discovery query", "error", err) return nil, query, err } return results, query, err case requestTypeIP: ip := dnsutil.IPFromARPA(qName) if ip == nil { r.logger.Error("error building IP from DNS request", "name", qName) return nil, nil, errNameNotFound } results, err := r.processor.QueryByIP(ip, discovery.Context{Token: reqCtx.Token}) return results, nil, err case requestTypeAddress: results, err := buildAddressResults(req) if err != nil { r.logger.Error("error processing discovery query", "error", err) return nil, nil, err } return results, nil, nil } r.logger.Error("error parsing discovery query type", "requestType", reqType) return nil, nil, errInvalidQuestion } // ServeDNS implements the miekg/dns.Handler interface. // This is a standard DNS listener, so we inject a default request context based on the agent's config. func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { reqCtx := r.defaultAgentDNSRequestContext() out := r.HandleRequest(req, reqCtx, w.RemoteAddr()) w.WriteMsg(out) } // ReloadConfig hot-reloads the router config with new parameters func (r *Router) ReloadConfig(newCfg *config.RuntimeConfig) error { cfg, err := getDynamicRouterConfig(newCfg) if err != nil { return fmt.Errorf("error loading DNS config: %w", err) } r.dynamicConfig.Store(cfg) return nil } // getTTLForService Find the TTL for a given service. // return ttl, true if found, 0, false otherwise func (cfg *RouterDynamicConfig) getTTLForService(service string) (time.Duration, bool) { if cfg.TTLStrict != nil { ttl, ok := cfg.TTLStrict[service] if ok { return ttl, true } } if cfg.TTLRadix != nil { _, ttlRaw, ok := cfg.TTLRadix.LongestPrefix(service) if ok { return ttlRaw.(time.Duration), true } } return 0, false } // Request type is similar to miekg/dns.Type, but correlates to the different query processors we might need to invoke. type requestType string const ( requestTypeName requestType = "NAME" // A/AAAA/CNAME/SRV requestTypeIP requestType = "IP" // PTR requestTypeAddress requestType = "ADDR" // Custom addr. A/AAAA lookups requestTypeConsul requestType = "CONSUL" // SOA/NS ) // parseDomain converts a DNS message into a generic discovery request. // If the request domain does not match "consul." or the alternative domain, // it will return true for needRecurse. The logic is based on miekg/dns.ServeDNS matcher. // The implementation assumes that the only valid domains are "consul." and the alternative domain, and // that DS query types are not supported. func (r *Router) parseDomain(questionName string) (string, bool) { target := dns.CanonicalName(questionName) target, _ = stripSuffix(target) for offset, overflow := 0, false; !overflow; offset, overflow = dns.NextLabel(target, offset) { subdomain := target[offset:] switch subdomain { case ".": // We don't support consul having a domain or altdomain attached to the root. return "", true case r.domain: return r.domain, false case r.altDomain: return r.altDomain, false case arpaDomain: // PTR queries always respond with the primary domain. return r.domain, false // Default: fallthrough } } // No match found; recurse if possible return "", true } // parseRequestType inspects the DNS message type and question name to determine the requestType of request. // We assume by the time this is called, we are responding to a question with a domain we serve. // This is used internally to determine which query processor method (if any) to invoke. func parseRequestType(req *dns.Msg) requestType { switch { case req.Question[0].Qtype == dns.TypeSOA || req.Question[0].Qtype == dns.TypeNS: // SOA and NS type supersede the domain // NOTE!: In V1 of the DNS server it was possible to serve a PTR lookup using the arpa domain but a SOA question type. // This also included the SOA record. This seemed inconsistent and unnecessary - it was removed for simplicity. return requestTypeConsul case isPTRSubdomain(req.Question[0].Name): return requestTypeIP case isAddrSubdomain(req.Question[0].Name): return requestTypeAddress default: return requestTypeName } } // serializeQueryResults converts a discovery.Result into a DNS message. func (r *Router) serializeQueryResults(req *dns.Msg, reqCtx Context, query *discovery.Query, results []*discovery.Result, cfg *RouterDynamicConfig, responseDomain string, remoteAddress net.Addr, maxRecursionLevel int) (*dns.Msg, error) { resp := new(dns.Msg) resp.SetReply(req) resp.Compress = !cfg.DisableCompression resp.Authoritative = true resp.RecursionAvailable = canRecurse(cfg) qType := req.Question[0].Qtype reqType := parseRequestType(req) // Always add the SOA record if requested. switch { case qType == dns.TypeSOA: resp.Answer = append(resp.Answer, makeSOARecord(responseDomain, cfg)) for _, result := range results { ans, ex, ns := r.getAnswerExtraAndNs(result, req, reqCtx, query, cfg, responseDomain, remoteAddress, maxRecursionLevel) resp.Answer = append(resp.Answer, ans...) resp.Extra = append(resp.Extra, ex...) resp.Ns = append(resp.Ns, ns...) } case reqType == requestTypeAddress: for _, result := range results { ans, ex, ns := r.getAnswerExtraAndNs(result, req, reqCtx, query, cfg, responseDomain, remoteAddress, maxRecursionLevel) resp.Answer = append(resp.Answer, ans...) resp.Extra = append(resp.Extra, ex...) resp.Ns = append(resp.Ns, ns...) } case qType == dns.TypeSRV: handled := make(map[string]struct{}) for _, result := range results { // Avoid duplicate entries, possible if a node has // the same service the same port, etc. // The datacenter should be empty during translation if it is a peering lookup. // This should be fine because we should always prefer the WAN address. //serviceAddress := d.agent.TranslateServiceAddress(lookup.Datacenter, node.Service.Address, node.Service.TaggedAddresses, TranslateAddressAcceptAny) //servicePort := d.agent.TranslateServicePort(lookup.Datacenter, node.Service.Port, node.Service.TaggedAddresses) //tuple := fmt.Sprintf("%s:%s:%d", node.Node.Node, serviceAddress, servicePort) tuple := fmt.Sprintf("%s:%s:%d", result.Node.Name, result.Service.Address, result.PortNumber) if _, ok := handled[tuple]; ok { continue } handled[tuple] = struct{}{} ans, ex, ns := r.getAnswerExtraAndNs(result, req, reqCtx, query, cfg, responseDomain, remoteAddress, maxRecursionLevel) resp.Answer = append(resp.Answer, ans...) resp.Extra = append(resp.Extra, ex...) resp.Ns = append(resp.Ns, ns...) } default: // default will send it to where it does some de-duping while it calls getAnswerExtraAndNs and recurses. r.appendResultsToDNSResponse(req, reqCtx, query, resp, results, cfg, responseDomain, remoteAddress, maxRecursionLevel) } return resp, nil } // appendResultsToDNSResponse builds dns message from the discovery results and // appends them to the dns response. func (r *Router) appendResultsToDNSResponse(req *dns.Msg, reqCtx Context, query *discovery.Query, resp *dns.Msg, results []*discovery.Result, cfg *RouterDynamicConfig, responseDomain string, remoteAddress net.Addr, maxRecursionLevel int) { // Always add the SOA record if requested. if req.Question[0].Qtype == dns.TypeSOA { resp.Answer = append(resp.Answer, makeSOARecord(responseDomain, cfg)) } handled := make(map[string]struct{}) var answerCNAME []dns.RR = nil count := 0 for _, result := range results { // Add the node record had_answer := false ans, extra, _ := r.getAnswerExtraAndNs(result, req, reqCtx, query, cfg, responseDomain, remoteAddress, maxRecursionLevel) resp.Extra = append(resp.Extra, extra...) if len(ans) == 0 { continue } // Avoid duplicate entries, possible if a node has // the same service on multiple ports, etc. if _, ok := handled[ans[0].String()]; ok { continue } handled[ans[0].String()] = struct{}{} switch ans[0].(type) { case *dns.CNAME: // keep track of the first CNAME + associated RRs but don't add to the resp.Answer yet // this will only be added if no non-CNAME RRs are found if len(answerCNAME) == 0 { answerCNAME = ans } default: resp.Answer = append(resp.Answer, ans...) had_answer = true } if had_answer { count++ if count == cfg.ARecordLimit { // We stop only if greater than 0 or we reached the limit return } } } if len(resp.Answer) == 0 && len(answerCNAME) > 0 { resp.Answer = answerCNAME } } // defaultAgentDNSRequestContext returns a default request context based on the agent's config. func (r *Router) defaultAgentDNSRequestContext() Context { return Context{ Token: r.tokenFunc(), // We don't need to specify the agent's partition here because that will be handled further down the stack // in the query processor. } } // resolveCNAME is used to recursively resolve CNAME records func (r *Router) resolveCNAME(cfg *RouterDynamicConfig, name string, reqCtx Context, remoteAddress net.Addr, maxRecursionLevel int) []dns.RR { // If the CNAME record points to a Consul address, resolve it internally // Convert query to lowercase because DNS is case-insensitive; d.domain and // d.altDomain are already converted if ln := strings.ToLower(name); strings.HasSuffix(ln, "."+r.domain) || strings.HasSuffix(ln, "."+r.altDomain) { if maxRecursionLevel < 1 { //d.logger.Error("Infinite recursion detected for name, won't perform any CNAME resolution.", "name", name) return nil } req := &dns.Msg{} req.SetQuestion(name, dns.TypeANY) // TODO: handle error response resp := r.handleRequestRecursively(req, reqCtx, nil, maxRecursionLevel-1) return resp.Answer } // Do nothing if we don't have a recursor if !canRecurse(cfg) { return nil } // Ask for any A records m := new(dns.Msg) m.SetQuestion(name, dns.TypeA) // Make a DNS lookup request recursorResponse, err := r.recursor.handle(m, cfg, remoteAddress) if err == nil { return recursorResponse.Answer } r.logger.Error("all resolvers failed for name", "name", name) return nil } // validateAndNormalizeRequest validates the DNS request and normalizes the request name. func validateAndNormalizeRequest(req *dns.Msg) error { // like upstream miekg/dns, we require at least one question, // but we will only answer the first. if len(req.Question) == 0 { return errInvalidQuestion } // We mutate the request name to respond with the canonical name. // This is Consul convention. req.Question[0].Name = dns.CanonicalName(req.Question[0].Name) return nil } // stripSuffix strips off the suffixes that may have been added to the request name. func stripSuffix(target string) (string, bool) { enableFailover := false // Strip off any suffixes that may have been added. offset, underflow := dns.PrevLabel(target, 1) if !underflow { maybeSuffix := target[offset:] switch maybeSuffix { case suffixFailover: target = target[:offset] enableFailover = true case suffixNoFailover: target = target[:offset] } } return target, enableFailover } // isAddrSubdomain returns true if the domain is a valid addr subdomain. func isAddrSubdomain(domain string) bool { labels := dns.SplitDomainName(domain) // Looking for .addr..consul. if len(labels) > 2 { return labels[1] == addrLabel } return false } // isPTRSubdomain returns true if the domain ends in the PTR domain, "in-addr.arpa.". func isPTRSubdomain(domain string) bool { labels := dns.SplitDomainName(domain) labelCount := len(labels) // We keep this check brief so we can have more specific error handling later. if labelCount < 1 { return false } return labels[labelCount-1] == arpaLabel } // getDynamicRouterConfig takes agent config and creates/resets the config used by DNS Router func getDynamicRouterConfig(conf *config.RuntimeConfig) (*RouterDynamicConfig, error) { cfg := &RouterDynamicConfig{ ARecordLimit: conf.DNSARecordLimit, EnableTruncate: conf.DNSEnableTruncate, NodeTTL: conf.DNSNodeTTL, RecursorStrategy: conf.DNSRecursorStrategy, RecursorTimeout: conf.DNSRecursorTimeout, UDPAnswerLimit: conf.DNSUDPAnswerLimit, NodeMetaTXT: conf.DNSNodeMetaTXT, DisableCompression: conf.DNSDisableCompression, SOAConfig: SOAConfig{ Expire: conf.DNSSOA.Expire, Minttl: conf.DNSSOA.Minttl, Refresh: conf.DNSSOA.Refresh, Retry: conf.DNSSOA.Retry, }, } if conf.DNSServiceTTL != nil { cfg.TTLRadix = radix.New() cfg.TTLStrict = make(map[string]time.Duration) for key, ttl := range conf.DNSServiceTTL { // All suffix with '*' are put in radix // This include '*' that will match anything if strings.HasSuffix(key, "*") { cfg.TTLRadix.Insert(key[:len(key)-1], ttl) } else { cfg.TTLStrict[key] = ttl } } } else { cfg.TTLRadix = nil cfg.TTLStrict = nil } for _, r := range conf.DNSRecursors { ra, err := formatRecursorAddress(r) if err != nil { return nil, fmt.Errorf("invalid recursor address: %w", err) } cfg.Recursors = append(cfg.Recursors, ra) } return cfg, nil } // canRecurse returns true if the router can recurse on the request. func canRecurse(cfg *RouterDynamicConfig) bool { return len(cfg.Recursors) > 0 } // createServerFailureResponse returns a SERVFAIL message. func createServerFailureResponse(req *dns.Msg, cfg *RouterDynamicConfig, recursionAvailable bool) *dns.Msg { // Return a SERVFAIL message m := &dns.Msg{} m.SetReply(req) m.Compress = !cfg.DisableCompression m.SetRcode(req, dns.RcodeServerFailure) m.RecursionAvailable = recursionAvailable if edns := req.IsEdns0(); edns != nil { setEDNS(req, m, true) } return m } // setEDNS is used to set the responses EDNS size headers and // possibly the ECS headers as well if they were present in the // original request func setEDNS(request *dns.Msg, response *dns.Msg, ecsGlobal bool) { edns := request.IsEdns0() if edns == nil { return } // cannot just use the SetEdns0 function as we need to embed // the ECS option as well ednsResp := new(dns.OPT) ednsResp.Hdr.Name = "." ednsResp.Hdr.Rrtype = dns.TypeOPT ednsResp.SetUDPSize(edns.UDPSize()) // Set up the ECS option if present if subnet := ednsSubnetForRequest(request); subnet != nil { subOp := new(dns.EDNS0_SUBNET) subOp.Code = dns.EDNS0SUBNET subOp.Family = subnet.Family subOp.Address = subnet.Address subOp.SourceNetmask = subnet.SourceNetmask if c := response.Rcode; ecsGlobal || c == dns.RcodeNameError || c == dns.RcodeServerFailure || c == dns.RcodeRefused || c == dns.RcodeNotImplemented { // reply is globally valid and should be cached accordingly subOp.SourceScope = 0 } else { // reply is only valid for the subnet it was queried with subOp.SourceScope = subnet.SourceNetmask } ednsResp.Option = append(ednsResp.Option, subOp) } response.Extra = append(response.Extra, ednsResp) } // ednsSubnetForRequest looks through the request to find any EDS subnet options func ednsSubnetForRequest(req *dns.Msg) *dns.EDNS0_SUBNET { // IsEdns0 returns the EDNS RR if present or nil otherwise edns := req.IsEdns0() if edns == nil { return nil } for _, o := range edns.Option { if subnet, ok := o.(*dns.EDNS0_SUBNET); ok { return subnet } } return nil } // createRefusedResponse returns a REFUSED message. This is the default behavior for unmatched queries in // upstream miekg/dns. func createRefusedResponse(req *dns.Msg) *dns.Msg { // Return a REFUSED message m := &dns.Msg{} m.SetRcode(req, dns.RcodeRefused) return m } // createAuthoritativeResponse returns an authoritative message that contains the SOA in the event that data is // not return for a query. There can be multiple reasons for not returning data, hence the rcode argument. func createAuthoritativeResponse(req *dns.Msg, cfg *RouterDynamicConfig, domain string, rcode int, ecsGlobal bool) *dns.Msg { m := &dns.Msg{} m.SetRcode(req, rcode) m.Compress = !cfg.DisableCompression m.Authoritative = true m.RecursionAvailable = canRecurse(cfg) if edns := req.IsEdns0(); edns != nil { setEDNS(req, m, ecsGlobal) } // We add the SOA on NameErrors soa := makeSOARecord(domain, cfg) m.Ns = append(m.Ns, soa) return m } // buildAddressResults returns a discovery.Result from a DNS request for addr. records. func buildAddressResults(req *dns.Msg) ([]*discovery.Result, error) { domain := dns.CanonicalName(req.Question[0].Name) labels := dns.SplitDomainName(domain) hexadecimal := labels[0] if len(hexadecimal)/2 != 4 && len(hexadecimal)/2 != 16 { return nil, errNameNotFound } var ip net.IP ip, err := hex.DecodeString(hexadecimal) if err != nil { return nil, errNameNotFound } return []*discovery.Result{ { Node: &discovery.Location{ Address: ip.String(), }, Type: discovery.ResultTypeNode, // We choose node by convention since we do not know the origin of the IP }, }, nil } // getAnswerAndExtra creates the dns answer and extra from discovery results. func (r *Router) getAnswerExtraAndNs(result *discovery.Result, req *dns.Msg, reqCtx Context, query *discovery.Query, cfg *RouterDynamicConfig, domain string, remoteAddress net.Addr, maxRecursionLevel int) (answer []dns.RR, extra []dns.RR, ns []dns.RR) { serviceAddress := newDNSAddress("") if result.Service != nil { serviceAddress = newDNSAddress(result.Service.Address) } nodeAddress := newDNSAddress("") if result.Node != nil { nodeAddress = newDNSAddress(result.Node.Address) } qName := req.Question[0].Name ttlLookupName := qName if query != nil { ttlLookupName = query.QueryPayload.Name } ttl := getTTLForResult(ttlLookupName, query, cfg) qType := req.Question[0].Qtype // TODO (v2-dns): skip records that refer to a workload/node that don't have a valid DNS name. // Special case responses switch { // PTR requests are first since they are a special case of domain overriding question type case parseRequestType(req) == requestTypeIP: ptrTarget := "" if result.Type == discovery.ResultTypeNode { ptrTarget = result.Node.Name } else if result.Type == discovery.ResultTypeService { ptrTarget = result.Service.Name } ptr := &dns.PTR{ Hdr: dns.RR_Header{Name: qName, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, Ptr: canonicalNameForResult(result.Type, ptrTarget, domain, result.Tenancy, result.PortName), } answer = append(answer, ptr) case qType == dns.TypeNS: // TODO (v2-dns): fqdn in V1 has the datacenter included, this would need to be added to discovery.Result resultType := result.Type target := result.Node.Name if parseRequestType(req) == requestTypeConsul && resultType == discovery.ResultTypeService { resultType = discovery.ResultTypeNode } fqdn := canonicalNameForResult(resultType, target, domain, result.Tenancy, result.PortName) extraRecord := makeIPBasedRecord(fqdn, nodeAddress, ttl) // TODO (v2-dns): this is not sufficient, because recursion and CNAMES are supported answer = append(answer, makeNSRecord(domain, fqdn, ttl)) extra = append(extra, extraRecord) case qType == dns.TypeSOA: // TODO (v2-dns): fqdn in V1 has the datacenter included, this would need to be added to discovery.Result // to be returned in the result. fqdn := canonicalNameForResult(result.Type, result.Node.Name, domain, result.Tenancy, result.PortName) extraRecord := makeIPBasedRecord(fqdn, nodeAddress, ttl) // TODO (v2-dns): this is not sufficient, because recursion and CNAMES are supported ns = append(ns, makeNSRecord(domain, fqdn, ttl)) extra = append(extra, extraRecord) case qType == dns.TypeSRV: // We put A/AAAA/CNAME records in the additional section for SRV requests a, e := r.getAnswerExtrasForAddressAndTarget(nodeAddress, serviceAddress, req, reqCtx, result, ttl, remoteAddress, cfg, domain, maxRecursionLevel) answer = append(answer, a...) extra = append(extra, e...) default: a, e := r.getAnswerExtrasForAddressAndTarget(nodeAddress, serviceAddress, req, reqCtx, result, ttl, remoteAddress, cfg, domain, maxRecursionLevel) answer = append(answer, a...) extra = append(extra, e...) } a, e := getAnswerAndExtraTXT(req, cfg, qName, result, ttl, domain, query) answer = append(answer, a...) extra = append(extra, e...) return } // getAnswerExtrasForAddressAndTarget creates the dns answer and extra from nodeAddress and serviceAddress dnsAddress pairs. func (r *Router) getAnswerExtrasForAddressAndTarget(nodeAddress *dnsAddress, serviceAddress *dnsAddress, req *dns.Msg, reqCtx Context, result *discovery.Result, ttl uint32, remoteAddress net.Addr, cfg *RouterDynamicConfig, domain string, maxRecursionLevel int) (answer []dns.RR, extra []dns.RR) { qName := req.Question[0].Name reqType := parseRequestType(req) switch { case (reqType == requestTypeAddress || result.Type == discovery.ResultTypeVirtual) && serviceAddress.IsEmptyString() && nodeAddress.IsIP(): a, e := getAnswerExtrasForIP(qName, nodeAddress, req.Question[0], reqType, result, ttl, domain) answer = append(answer, a...) extra = append(extra, e...) case result.Type == discovery.ResultTypeNode && nodeAddress.IsIP(): canonicalNodeName := canonicalNameForResult(result.Type, result.Node.Name, domain, result.Tenancy, result.PortName) a, e := getAnswerExtrasForIP(canonicalNodeName, nodeAddress, req.Question[0], reqType, result, ttl, domain) answer = append(answer, a...) extra = append(extra, e...) case result.Type == discovery.ResultTypeNode && !nodeAddress.IsIP(): a, e := r.makeRecordFromFQDN(serviceAddress.FQDN(), result, req, reqCtx, cfg, ttl, remoteAddress, maxRecursionLevel) answer = append(answer, a...) extra = append(extra, e...) case serviceAddress.IsEmptyString() && nodeAddress.IsEmptyString(): return nil, nil // There is no service address and the node address is an IP case serviceAddress.IsEmptyString() && nodeAddress.IsIP(): canonicalNodeName := canonicalNameForResult(discovery.ResultTypeNode, result.Node.Name, domain, result.Tenancy, result.PortName) a, e := getAnswerExtrasForIP(canonicalNodeName, nodeAddress, req.Question[0], reqType, result, ttl, domain) answer = append(answer, a...) extra = append(extra, e...) // There is no service address and the node address is a FQDN (external service) case serviceAddress.IsEmptyString(): a, e := r.makeRecordFromFQDN(nodeAddress.FQDN(), result, req, reqCtx, cfg, ttl, remoteAddress, maxRecursionLevel) answer = append(answer, a...) extra = append(extra, e...) // The service address is an IP case serviceAddress.IsIP(): canonicalServiceName := canonicalNameForResult(discovery.ResultTypeService, result.Service.Name, domain, result.Tenancy, result.PortName) a, e := getAnswerExtrasForIP(canonicalServiceName, serviceAddress, req.Question[0], reqType, result, ttl, domain) answer = append(answer, a...) extra = append(extra, e...) // If the service address is a CNAME for the service we are looking // for then use the node address. case serviceAddress.FQDN() == req.Question[0].Name && nodeAddress.IsIP(): canonicalNodeName := canonicalNameForResult(discovery.ResultTypeNode, result.Node.Name, domain, result.Tenancy, result.PortName) a, e := getAnswerExtrasForIP(canonicalNodeName, nodeAddress, req.Question[0], reqType, result, ttl, domain) answer = append(answer, a...) extra = append(extra, e...) // The service address is a FQDN (internal or external service name) default: a, e := r.makeRecordFromFQDN(serviceAddress.FQDN(), result, req, reqCtx, cfg, ttl, remoteAddress, maxRecursionLevel) answer = append(answer, a...) extra = append(extra, e...) } return } // getAnswerAndExtraTXT determines whether a TXT needs to be create and then // returns the TXT record in the answer or extra depending on the question type. func getAnswerAndExtraTXT(req *dns.Msg, cfg *RouterDynamicConfig, qName string, result *discovery.Result, ttl uint32, domain string, query *discovery.Query) (answer []dns.RR, extra []dns.RR) { if !shouldAppendTXTRecord(query, cfg, req) { return } recordHeaderName := qName serviceAddress := newDNSAddress("") if result.Service != nil { serviceAddress = newDNSAddress(result.Service.Address) } if result.Type != discovery.ResultTypeNode && result.Type != discovery.ResultTypeVirtual && !serviceAddress.IsInternalFQDN(domain) && !serviceAddress.IsExternalFQDN(domain) { recordHeaderName = canonicalNameForResult(discovery.ResultTypeNode, result.Node.Name, domain, result.Tenancy, result.PortName) } qType := req.Question[0].Qtype generateMeta := false metaInAnswer := false if qType == dns.TypeANY || qType == dns.TypeTXT { generateMeta = true metaInAnswer = true } else if cfg.NodeMetaTXT { generateMeta = true } // Do not generate txt records if we don't have to: https://github.com/hashicorp/consul/pull/5272 if generateMeta { meta := makeTXTRecord(recordHeaderName, result, ttl) if metaInAnswer { answer = append(answer, meta...) } else { extra = append(extra, meta...) } } return answer, extra } // shouldAppendTXTRecord determines whether a TXT record should be appended to the response. func shouldAppendTXTRecord(query *discovery.Query, cfg *RouterDynamicConfig, req *dns.Msg) bool { qType := req.Question[0].Qtype switch { // Node records case query != nil && query.QueryType == discovery.QueryTypeNode && (cfg.NodeMetaTXT || qType == dns.TypeANY || qType == dns.TypeTXT): return true // Service records case query != nil && query.QueryType == discovery.QueryTypeService && cfg.NodeMetaTXT && qType == dns.TypeSRV: return true // Prepared query records case query != nil && query.QueryType == discovery.QueryTypePreparedQuery && cfg.NodeMetaTXT && qType == dns.TypeSRV: return true } return false } // getAnswerExtrasForIP creates the dns answer and extra from IP dnsAddress pairs. func getAnswerExtrasForIP(name string, addr *dnsAddress, question dns.Question, reqType requestType, result *discovery.Result, ttl uint32, _ string) (answer []dns.RR, extra []dns.RR) { qType := question.Qtype // Have to pass original question name here even if the system has recursed // and stripped off the domain suffix. recHdrName := question.Name if qType == dns.TypeSRV { nameSplit := strings.Split(name, ".") if len(nameSplit) > 1 && nameSplit[1] == addrLabel { recHdrName = name } else { recHdrName = name } name = question.Name } record := makeIPBasedRecord(recHdrName, addr, ttl) isARecordWhenNotExplicitlyQueried := record.Header().Rrtype == dns.TypeA && qType != dns.TypeA && qType != dns.TypeANY isAAAARecordWhenNotExplicitlyQueried := record.Header().Rrtype == dns.TypeAAAA && qType != dns.TypeAAAA && qType != dns.TypeANY // For explicit A/AAAA queries, we must only return those records in the answer section. if isARecordWhenNotExplicitlyQueried || isAAAARecordWhenNotExplicitlyQueried { extra = append(extra, record) } else { answer = append(answer, record) } if reqType != requestTypeAddress && qType == dns.TypeSRV { srv := makeSRVRecord(name, recHdrName, result, ttl) answer = append(answer, srv) } return } // encodeIPAsFqdn encodes an IP address as a FQDN. func encodeIPAsFqdn(result *discovery.Result, ip net.IP, responseDomain string) string { ipv4 := ip.To4() ipStr := hex.EncodeToString(ip) if ipv4 != nil { ipStr = ipStr[len(ipStr)-(net.IPv4len*2):] } if result.Tenancy.PeerName != "" { // Exclude the datacenter from the FQDN on the addr for peers. // This technically makes no difference, since the addr endpoint ignores the DC // component of the request, but do it anyway for a less confusing experience. return fmt.Sprintf("%s.addr.%s", ipStr, responseDomain) } return fmt.Sprintf("%s.addr.%s.%s", ipStr, result.Tenancy.Datacenter, responseDomain) } func makeSOARecord(domain string, cfg *RouterDynamicConfig) dns.RR { return &dns.SOA{ Hdr: dns.RR_Header{ Name: domain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, // Has to be consistent with MinTTL to avoid invalidation Ttl: cfg.SOAConfig.Minttl, }, Ns: "ns." + domain, Serial: uint32(time.Now().Unix()), Mbox: "hostmaster." + domain, Refresh: cfg.SOAConfig.Refresh, Retry: cfg.SOAConfig.Retry, Expire: cfg.SOAConfig.Expire, Minttl: cfg.SOAConfig.Minttl, } } func makeNSRecord(domain, fqdn string, ttl uint32) dns.RR { return &dns.NS{ Hdr: dns.RR_Header{ Name: domain, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: ttl, }, Ns: fqdn, } } // makeIPBasedRecord an A or AAAA record for the given name and IP. // Note: we might want to pass in the Query Name here, which is used in addr. and virtual. queries // since there is only ever one result. Right now choosing to leave it off for simplification. func makeIPBasedRecord(name string, addr *dnsAddress, ttl uint32) dns.RR { if addr.IsIPV4() { // check if the query type is A for IPv4 or ANY return &dns.A{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl, }, A: addr.IP(), } } return &dns.AAAA{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl, }, AAAA: addr.IP(), } } func (r *Router) makeRecordFromFQDN(fqdn string, result *discovery.Result, req *dns.Msg, reqCtx Context, cfg *RouterDynamicConfig, ttl uint32, remoteAddress net.Addr, maxRecursionLevel int) ([]dns.RR, []dns.RR) { edns := req.IsEdns0() != nil q := req.Question[0] more := r.resolveCNAME(cfg, dns.Fqdn(fqdn), reqCtx, remoteAddress, maxRecursionLevel) var additional []dns.RR extra := 0 MORE_REC: for _, rr := range more { switch rr.Header().Rrtype { case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA, dns.TypeTXT: // set the TTL manually rr.Header().Ttl = ttl additional = append(additional, rr) extra++ if extra == maxRecurseRecords && !edns { break MORE_REC } } } if q.Qtype == dns.TypeSRV { answers := []dns.RR{ makeSRVRecord(q.Name, fqdn, result, ttl), } return answers, additional } address := "" if result.Service != nil && result.Service.Address != "" { address = result.Service.Address } else if result.Node != nil { address = result.Node.Address } answers := []dns.RR{ makeCNAMERecord(q.Name, address, ttl), } answers = append(answers, additional...) return answers, nil } // makeCNAMERecord returns a CNAME record for the given name and target. func makeCNAMERecord(name string, target string, ttl uint32) *dns.CNAME { return &dns.CNAME{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: ttl, }, Target: dns.Fqdn(target), } } // func makeSRVRecord returns an SRV record for the given name and target. func makeSRVRecord(name, target string, result *discovery.Result, ttl uint32) *dns.SRV { return &dns.SRV{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: ttl, }, Priority: 1, Weight: uint16(result.Weight), Port: uint16(result.PortNumber), Target: target, } } // encodeKVasRFC1464 encodes a key-value pair according to RFC1464 func encodeKVasRFC1464(key, value string) (txt string) { // For details on these replacements c.f. https://www.ietf.org/rfc/rfc1464.txt key = strings.Replace(key, "`", "``", -1) key = strings.Replace(key, "=", "`=", -1) // Backquote the leading spaces leadingSpacesRE := regexp.MustCompile("^ +") numLeadingSpaces := len(leadingSpacesRE.FindString(key)) key = leadingSpacesRE.ReplaceAllString(key, strings.Repeat("` ", numLeadingSpaces)) // Backquote the trailing spaces numTrailingSpaces := len(trailingSpacesRE.FindString(key)) key = trailingSpacesRE.ReplaceAllString(key, strings.Repeat("` ", numTrailingSpaces)) value = strings.Replace(value, "`", "``", -1) return key + "=" + value } // makeTXTRecord returns a TXT record for the given name and result metadata. func makeTXTRecord(name string, result *discovery.Result, ttl uint32) []dns.RR { extra := make([]dns.RR, 0, len(result.Metadata)) for key, value := range result.Metadata { txt := value if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") { txt = encodeKVasRFC1464(key, value) } extra = append(extra, &dns.TXT{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: ttl, }, Txt: []string{txt}, }) } return extra }