-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Query node_meta info using DNS TXT query API #3343
Changes from all commits
38bc9c1
cde5f0d
31901c3
22de633
b3689da
67d07fd
257eadb
aa79189
94b6443
bf96e1e
39e37d0
faf31cb
629aa20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -341,7 +341,8 @@ func (d *DNSServer) nameservers(edns bool) (ns []dns.RR, extra []dns.RR) { | |
} | ||
ns = append(ns, nsrr) | ||
|
||
glue := d.formatNodeRecord(addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns) | ||
// A or AAAA glue record | ||
glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns) | ||
extra = append(extra, glue...) | ||
|
||
// don't provide more than 3 servers | ||
|
@@ -485,9 +486,9 @@ INVALID: | |
|
||
// nodeLookup is used to handle a node query | ||
func (d *DNSServer) nodeLookup(network, datacenter, node string, req, resp *dns.Msg) { | ||
// Only handle ANY, A and AAAA type requests | ||
// Only handle ANY, A, AAAA, and TXT type requests | ||
qType := req.Question[0].Qtype | ||
if qType != dns.TypeANY && qType != dns.TypeA && qType != dns.TypeAAAA { | ||
if qType != dns.TypeANY && qType != dns.TypeA && qType != dns.TypeAAAA && qType != dns.TypeTXT { | ||
return | ||
} | ||
|
||
|
@@ -530,45 +531,67 @@ RPC: | |
n := out.NodeServices.Node | ||
edns := req.IsEdns0() != nil | ||
addr := d.agent.TranslateAddress(datacenter, n.Address, n.TaggedAddresses) | ||
records := d.formatNodeRecord(addr, req.Question[0].Name, qType, d.config.NodeTTL, edns) | ||
records := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns) | ||
if records != nil { | ||
resp.Answer = append(resp.Answer, records...) | ||
} | ||
} | ||
|
||
// formatNodeRecord takes a Node and returns an A, AAAA, or CNAME record | ||
func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time.Duration, edns bool) (records []dns.RR) { | ||
// 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 | ||
trailingSpacesRE := regexp.MustCompile(" +$") | ||
numTrailingSpaces := len(trailingSpacesRE.FindString(key)) | ||
key = trailingSpacesRE.ReplaceAllString(key, strings.Repeat("` ", numTrailingSpaces)) | ||
|
||
value = strings.Replace(value, "`", "``", -1) | ||
|
||
return key + "=" + value | ||
} | ||
|
||
// formatNodeRecord takes a Node and returns an A, AAAA, TXT or CNAME record | ||
func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns bool) (records []dns.RR) { | ||
// Parse the IP | ||
ip := net.ParseIP(addr) | ||
var ipv4 net.IP | ||
if ip != nil { | ||
ipv4 = ip.To4() | ||
} | ||
|
||
switch { | ||
case ipv4 != nil && (qType == dns.TypeANY || qType == dns.TypeA): | ||
return []dns.RR{&dns.A{ | ||
records = append(records, &dns.A{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have to append in order to support ANY queries when the node also has metadata key-values |
||
Hdr: dns.RR_Header{ | ||
Name: qName, | ||
Rrtype: dns.TypeA, | ||
Class: dns.ClassINET, | ||
Ttl: uint32(ttl / time.Second), | ||
}, | ||
A: ip, | ||
}} | ||
}) | ||
|
||
case ip != nil && ipv4 == nil && (qType == dns.TypeANY || qType == dns.TypeAAAA): | ||
return []dns.RR{&dns.AAAA{ | ||
records = append(records, &dns.AAAA{ | ||
Hdr: dns.RR_Header{ | ||
Name: qName, | ||
Rrtype: dns.TypeAAAA, | ||
Class: dns.ClassINET, | ||
Ttl: uint32(ttl / time.Second), | ||
}, | ||
AAAA: ip, | ||
}} | ||
}) | ||
|
||
case ip == nil && (qType == dns.TypeANY || qType == dns.TypeCNAME || | ||
qType == dns.TypeA || qType == dns.TypeAAAA): | ||
qType == dns.TypeA || qType == dns.TypeAAAA || qType == dns.TypeTXT): | ||
// Get the CNAME | ||
cnRec := &dns.CNAME{ | ||
Hdr: dns.RR_Header{ | ||
|
@@ -587,7 +610,7 @@ func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time. | |
MORE_REC: | ||
for _, rr := range more { | ||
switch rr.Header().Rrtype { | ||
case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA: | ||
case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA, dns.TypeTXT: | ||
records = append(records, rr) | ||
extra++ | ||
if extra == maxRecurseRecords && !edns { | ||
|
@@ -596,6 +619,25 @@ func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time. | |
} | ||
} | ||
} | ||
|
||
if node != nil && (qType == dns.TypeANY || qType == dns.TypeTXT) { | ||
for key, value := range node.Meta { | ||
txt := value | ||
if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") { | ||
txt = encodeKVasRFC1464(key, value) | ||
} | ||
records = append(records, &dns.TXT{ | ||
Hdr: dns.RR_Header{ | ||
Name: qName, | ||
Rrtype: dns.TypeTXT, | ||
Class: dns.ClassINET, | ||
Ttl: uint32(ttl / time.Second), | ||
}, | ||
Txt: []string{txt}, | ||
}) | ||
} | ||
} | ||
|
||
return records | ||
} | ||
|
||
|
@@ -929,7 +971,7 @@ func (d *DNSServer) serviceNodeRecords(dc string, nodes structs.CheckServiceNode | |
handled[addr] = struct{}{} | ||
|
||
// Add the node record | ||
records := d.formatNodeRecord(addr, qName, qType, ttl, edns) | ||
records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Undo |
||
if records != nil { | ||
resp.Answer = append(resp.Answer, records...) | ||
} | ||
|
@@ -973,7 +1015,7 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes | |
} | ||
|
||
// Add the extra record | ||
records := d.formatNodeRecord(addr, srvRec.Target, dns.TypeANY, ttl, edns) | ||
records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Undo |
||
if len(records) > 0 { | ||
// Use the node address if it doesn't differ from the service address | ||
if addr == node.Node.Address { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,6 +78,18 @@ func dnsA(src, dest string) *dns.A { | |
} | ||
} | ||
|
||
// dnsTXT returns a DNS TXT record struct | ||
func dnsTXT(src string, txt []string) *dns.TXT { | ||
return &dns.TXT{ | ||
Hdr: dns.RR_Header{ | ||
Name: dns.Fqdn(src), | ||
Rrtype: dns.TypeTXT, | ||
Class: dns.ClassINET, | ||
}, | ||
Txt: txt, | ||
} | ||
} | ||
|
||
func TestRecursorAddr(t *testing.T) { | ||
t.Parallel() | ||
addr, err := recursorAddr("8.8.8.8") | ||
|
@@ -89,6 +101,35 @@ func TestRecursorAddr(t *testing.T) { | |
} | ||
} | ||
|
||
func TestEncodeKVasRFC1464(t *testing.T) { | ||
// Test cases are from rfc1464 | ||
type rfc1464Test struct { | ||
key, value, internalForm, externalForm string | ||
} | ||
tests := []rfc1464Test{ | ||
{"color", "blue", "color=blue", "color=blue"}, | ||
{"equation", "a=4", "equation=a=4", "equation=a=4"}, | ||
{"a=a", "true", "a`=a=true", "a`=a=true"}, | ||
{"a\\=a", "false", "a\\`=a=false", "a\\`=a=false"}, | ||
{"=", "\\=", "`==\\=", "`==\\="}, | ||
|
||
{"string", "\"Cat\"", "string=\"Cat\"", "string=\"Cat\""}, | ||
{"string2", "`abc`", "string2=``abc``", "string2=``abc``"}, | ||
{"novalue", "", "novalue=", "novalue="}, | ||
{"a b", "c d", "a b=c d", "a b=c d"}, | ||
{"abc ", "123 ", "abc` =123 ", "abc` =123 "}, | ||
|
||
// Additional tests | ||
{" abc", " 321", "` abc= 321", "` abc= 321"}, | ||
{"`a", "b", "``a=b", "``a=b"}, | ||
} | ||
|
||
for _, test := range tests { | ||
answer := encodeKVasRFC1464(test.key, test.value) | ||
verify.Values(t, "internalForm", answer, test.internalForm) | ||
} | ||
} | ||
|
||
func TestDNS_NodeLookup(t *testing.T) { | ||
t.Parallel() | ||
a := NewTestAgent(t.Name(), "") | ||
|
@@ -300,6 +341,7 @@ func TestDNS_NodeLookup_CNAME(t *testing.T) { | |
Answer: []dns.RR{ | ||
dnsCNAME("www.google.com", "google.com"), | ||
dnsA("google.com", "1.2.3.4"), | ||
dnsTXT("google.com", []string{"my_txt_value"}), | ||
}, | ||
}) | ||
defer recursor.Shutdown() | ||
|
@@ -330,23 +372,117 @@ func TestDNS_NodeLookup_CNAME(t *testing.T) { | |
t.Fatalf("err: %v", err) | ||
} | ||
|
||
// Should have the service record, CNAME record + A record | ||
if len(in.Answer) != 3 { | ||
wantAnswer := []dns.RR{ | ||
&dns.CNAME{ | ||
Hdr: dns.RR_Header{Name: "google.node.consul.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 0, Rdlength: 0x10}, | ||
Target: "www.google.com.", | ||
}, | ||
&dns.CNAME{ | ||
Hdr: dns.RR_Header{Name: "www.google.com.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Rdlength: 0x2}, | ||
Target: "google.com.", | ||
}, | ||
&dns.A{ | ||
Hdr: dns.RR_Header{Name: "google.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4}, | ||
A: []byte{0x1, 0x2, 0x3, 0x4}, // 1.2.3.4 | ||
}, | ||
&dns.TXT{ | ||
Hdr: dns.RR_Header{Name: "google.com.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Rdlength: 0xd}, | ||
Txt: []string{"my_txt_value"}, | ||
}, | ||
} | ||
verify.Values(t, "answer", in.Answer, wantAnswer) | ||
} | ||
|
||
func TestDNS_NodeLookup_TXT(t *testing.T) { | ||
cfg := TestConfig() | ||
a := NewTestAgent(t.Name(), cfg) | ||
defer a.Shutdown() | ||
|
||
args := &structs.RegisterRequest{ | ||
Datacenter: "dc1", | ||
Node: "google", | ||
Address: "127.0.0.1", | ||
NodeMeta: map[string]string{ | ||
"rfc1035-00": "value0", | ||
"key0": "value1", | ||
}, | ||
} | ||
|
||
var out struct{} | ||
if err := a.RPC("Catalog.Register", args, &out); err != nil { | ||
t.Fatalf("err: %v", err) | ||
} | ||
|
||
m := new(dns.Msg) | ||
m.SetQuestion("google.node.consul.", dns.TypeTXT) | ||
|
||
c := new(dns.Client) | ||
addr, _ := a.Config.ClientListener("", a.Config.Ports.DNS) | ||
in, _, err := c.Exchange(m, addr.String()) | ||
if err != nil { | ||
t.Fatalf("err: %v", err) | ||
} | ||
|
||
// Should have the 1 TXT record reply | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pls use |
||
if len(in.Answer) != 2 { | ||
t.Fatalf("Bad: %#v", in) | ||
} | ||
|
||
cnRec, ok := in.Answer[0].(*dns.CNAME) | ||
txtRec, ok := in.Answer[0].(*dns.TXT) | ||
if !ok { | ||
t.Fatalf("Bad: %#v", in.Answer[0]) | ||
} | ||
if cnRec.Target != "www.google.com." { | ||
if len(txtRec.Txt) != 1 { | ||
t.Fatalf("Bad: %#v", in.Answer[0]) | ||
} | ||
if cnRec.Hdr.Ttl != 0 { | ||
if txtRec.Txt[0] != "value0" && txtRec.Txt[0] != "key0=value1" { | ||
t.Fatalf("Bad: %#v", in.Answer[0]) | ||
} | ||
} | ||
|
||
func TestDNS_NodeLookup_ANY(t *testing.T) { | ||
cfg := TestConfig() | ||
a := NewTestAgent(t.Name(), cfg) | ||
defer a.Shutdown() | ||
|
||
args := &structs.RegisterRequest{ | ||
Datacenter: "dc1", | ||
Node: "bar", | ||
Address: "127.0.0.1", | ||
NodeMeta: map[string]string{ | ||
"key": "value", | ||
}, | ||
} | ||
|
||
var out struct{} | ||
if err := a.RPC("Catalog.Register", args, &out); err != nil { | ||
t.Fatalf("err: %v", err) | ||
} | ||
|
||
m := new(dns.Msg) | ||
m.SetQuestion("bar.node.consul.", dns.TypeANY) | ||
|
||
c := new(dns.Client) | ||
addr, _ := a.Config.ClientListener("", a.Config.Ports.DNS) | ||
in, _, err := c.Exchange(m, addr.String()) | ||
if err != nil { | ||
t.Fatalf("err: %v", err) | ||
} | ||
|
||
wantAnswer := []dns.RR{ | ||
&dns.A{ | ||
Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4}, | ||
A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1 | ||
}, | ||
&dns.TXT{ | ||
Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Rdlength: 0xa}, | ||
Txt: []string{"key=value"}, | ||
}, | ||
} | ||
verify.Values(t, "answer", in.Answer, wantAnswer) | ||
|
||
} | ||
|
||
func TestDNS_EDNS0(t *testing.T) { | ||
t.Parallel() | ||
a := NewTestAgent(t.Name(), "") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,8 +57,9 @@ we can instead use `foo.node.consul.` This convention allows for terse | |
syntax where appropriate while supporting queries of nodes in remote | ||
datacenters as necessary. | ||
|
||
For a node lookup, the only records returned are A records containing | ||
the IP address of the node. | ||
For a node lookup, the only records returned are A and AAAA records | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added language for 'AAAA' records as well since it is in available in the code. Feel free to remove that in case it is not officially supported yet. |
||
containing the IP address, and TXT records containing the | ||
`node_meta` values of the node. | ||
|
||
```text | ||
$ dig @127.0.0.1 -p 8600 foo.node.consul ANY | ||
|
@@ -76,11 +77,19 @@ $ dig @127.0.0.1 -p 8600 foo.node.consul ANY | |
|
||
;; ANSWER SECTION: | ||
foo.node.consul. 0 IN A 10.1.10.12 | ||
foo.node.consul. 0 IN TXT "meta_key=meta_value" | ||
foo.node.consul. 0 IN TXT "value only" | ||
|
||
|
||
;; AUTHORITY SECTION: | ||
consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 | ||
``` | ||
|
||
By default the TXT records value will match the node's metadata key-value | ||
pairs according to [RFC1464](https://www.ietf.org/rfc/rfc1464.txt). | ||
Alternatively, the TXT record will only include the node's metadata value when the | ||
node's metadata key starts with `rfc1035-`. | ||
|
||
## Service Lookups | ||
|
||
A service lookup is used to query for service providers. Service queries support | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Undo
formatNodeRecord
change from commit 76319f7