Merge pull request #11348 from kbabuadze/fix-answers-alt-domain

Fix answers for alt domain
This commit is contained in:
Jared Kirschner 2021-10-29 17:09:20 -04:00 committed by GitHub
commit 0854e1d684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 388 additions and 28 deletions

3
.changelog/11348.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
dns: Fixed an issue where on DNS requests made with .alt_domain response was returned as .domain
```

View File

@ -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)...)
}
}
}

View File

@ -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])
}
}
}

View File

@ -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).

View File

@ -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),