Merge pull request #3343 from zeroae/f-node-dns-txt-record

This commit is contained in:
Frank Schroeder 2017-09-28 12:45:31 +02:00
commit eb6be8c0f1
No known key found for this signature in database
GPG Key ID: 4D65C6EAEC87DECD
4 changed files with 205 additions and 21 deletions

View File

@ -341,7 +341,7 @@ func (d *DNSServer) nameservers(edns bool) (ns []dns.RR, extra []dns.RR) {
} }
ns = append(ns, nsrr) ns = append(ns, nsrr)
glue := d.formatNodeRecord(addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns) glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns)
extra = append(extra, glue...) extra = append(extra, glue...)
// don't provide more than 3 servers // don't provide more than 3 servers
@ -485,9 +485,9 @@ INVALID:
// nodeLookup is used to handle a node query // nodeLookup is used to handle a node query
func (d *DNSServer) nodeLookup(network, datacenter, node string, req, resp *dns.Msg) { 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 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 return
} }
@ -530,23 +530,45 @@ RPC:
n := out.NodeServices.Node n := out.NodeServices.Node
edns := req.IsEdns0() != nil edns := req.IsEdns0() != nil
addr := d.agent.TranslateAddress(datacenter, n.Address, n.TaggedAddresses) 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 { if records != nil {
resp.Answer = append(resp.Answer, records...) resp.Answer = append(resp.Answer, records...)
} }
} }
// formatNodeRecord takes a Node and returns an A, AAAA, or CNAME record // encodeKVasRFC1464 encodes a key-value pair according to RFC1464
func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time.Duration, edns bool) (records []dns.RR) { 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 // Parse the IP
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
var ipv4 net.IP var ipv4 net.IP
if ip != nil { if ip != nil {
ipv4 = ip.To4() ipv4 = ip.To4()
} }
switch { switch {
case ipv4 != nil && (qType == dns.TypeANY || qType == dns.TypeA): case ipv4 != nil && (qType == dns.TypeANY || qType == dns.TypeA):
return []dns.RR{&dns.A{ records = append(records, &dns.A{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
Name: qName, Name: qName,
Rrtype: dns.TypeA, Rrtype: dns.TypeA,
@ -554,10 +576,10 @@ func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time.
Ttl: uint32(ttl / time.Second), Ttl: uint32(ttl / time.Second),
}, },
A: ip, A: ip,
}} })
case ip != nil && ipv4 == nil && (qType == dns.TypeANY || qType == dns.TypeAAAA): 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{ Hdr: dns.RR_Header{
Name: qName, Name: qName,
Rrtype: dns.TypeAAAA, Rrtype: dns.TypeAAAA,
@ -565,10 +587,10 @@ func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time.
Ttl: uint32(ttl / time.Second), Ttl: uint32(ttl / time.Second),
}, },
AAAA: ip, AAAA: ip,
}} })
case ip == nil && (qType == dns.TypeANY || qType == dns.TypeCNAME || 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 // Get the CNAME
cnRec := &dns.CNAME{ cnRec := &dns.CNAME{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
@ -587,7 +609,7 @@ func (d *DNSServer) formatNodeRecord(addr, qName string, qType uint16, ttl time.
MORE_REC: MORE_REC:
for _, rr := range more { for _, rr := range more {
switch rr.Header().Rrtype { switch rr.Header().Rrtype {
case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA: case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA, dns.TypeTXT:
records = append(records, rr) records = append(records, rr)
extra++ extra++
if extra == maxRecurseRecords && !edns { if extra == maxRecurseRecords && !edns {
@ -596,6 +618,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 return records
} }
@ -929,7 +970,7 @@ func (d *DNSServer) serviceNodeRecords(dc string, nodes structs.CheckServiceNode
handled[addr] = struct{}{} handled[addr] = struct{}{}
// Add the node record // Add the node record
records := d.formatNodeRecord(addr, qName, qType, ttl, edns) records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns)
if records != nil { if records != nil {
resp.Answer = append(resp.Answer, records...) resp.Answer = append(resp.Answer, records...)
} }
@ -973,7 +1014,7 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes
} }
// Add the extra record // 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)
if len(records) > 0 { if len(records) > 0 {
// Use the node address if it doesn't differ from the service address // Use the node address if it doesn't differ from the service address
if addr == node.Node.Address { if addr == node.Node.Address {

View File

@ -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) { func TestRecursorAddr(t *testing.T) {
t.Parallel() t.Parallel()
addr, err := recursorAddr("8.8.8.8") 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) { func TestDNS_NodeLookup(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestAgent(t.Name(), "") a := NewTestAgent(t.Name(), "")
@ -300,6 +341,7 @@ func TestDNS_NodeLookup_CNAME(t *testing.T) {
Answer: []dns.RR{ Answer: []dns.RR{
dnsCNAME("www.google.com", "google.com"), dnsCNAME("www.google.com", "google.com"),
dnsA("google.com", "1.2.3.4"), dnsA("google.com", "1.2.3.4"),
dnsTXT("google.com", []string{"my_txt_value"}),
}, },
}) })
defer recursor.Shutdown() defer recursor.Shutdown()
@ -330,23 +372,113 @@ func TestDNS_NodeLookup_CNAME(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
// Should have the service record, CNAME record + A record wantAnswer := []dns.RR{
if len(in.Answer) != 3 { &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) {
a := NewTestAgent(t.Name(), ``)
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)
in, _, err := c.Exchange(m, a.DNSAddr())
if err != nil {
t.Fatalf("err: %v", err)
}
// Should have the 1 TXT record reply
if len(in.Answer) != 2 {
t.Fatalf("Bad: %#v", in) t.Fatalf("Bad: %#v", in)
} }
cnRec, ok := in.Answer[0].(*dns.CNAME) txtRec, ok := in.Answer[0].(*dns.TXT)
if !ok { if !ok {
t.Fatalf("Bad: %#v", in.Answer[0]) 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]) 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]) t.Fatalf("Bad: %#v", in.Answer[0])
} }
} }
func TestDNS_NodeLookup_ANY(t *testing.T) {
a := NewTestAgent(t.Name(), ``)
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)
in, _, err := c.Exchange(m, a.DNSAddr())
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) { func TestDNS_EDNS0(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestAgent(t.Name(), "") a := NewTestAgent(t.Name(), "")

View File

@ -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 syntax where appropriate while supporting queries of nodes in remote
datacenters as necessary. datacenters as necessary.
For a node lookup, the only records returned are A records containing For a node lookup, the only records returned are A and AAAA records
the IP address of the node. containing the IP address, and TXT records containing the
`node_meta` values of the node.
```text ```text
$ dig @127.0.0.1 -p 8600 foo.node.consul ANY $ 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: ;; ANSWER SECTION:
foo.node.consul. 0 IN A 10.1.10.12 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: ;; AUTHORITY SECTION:
consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 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 ## Service Lookups
A service lookup is used to query for service providers. Service queries support A service lookup is used to query for service providers. Service queries support

View File

@ -432,6 +432,8 @@ will exit with an error at startup.
- Metadata keys must contain only alphanumeric, `-`, and `_` characters. - Metadata keys must contain only alphanumeric, `-`, and `_` characters.
- Metadata keys must not begin with the `consul-` prefix; that is reserved for internal use by Consul. - Metadata keys must not begin with the `consul-` prefix; that is reserved for internal use by Consul.
- Metadata values must be between 0 and 512 (inclusive) characters in length. - Metadata values must be between 0 and 512 (inclusive) characters in length.
- Metadata values for keys begining with `rfc1035-` are encoded verbatim in DNS TXT requests, otherwise
the metadata kv-pair is encoded according [RFC1464](https://www.ietf.org/rfc/rfc1464.txt).
* <a name="_pid_file"></a><a href="#_pid_file">`-pid-file`</a> - This flag provides the file * <a name="_pid_file"></a><a href="#_pid_file">`-pid-file`</a> - This flag provides the file
path for the agent to store its PID. This is useful for sending signals (for example, `SIGINT` path for the agent to store its PID. This is useful for sending signals (for example, `SIGINT`