Merge pull request #1813 from hashicorp/f-dns-trim

Makes sure UDP DNS responses aren't larger than allowed.
This commit is contained in:
James Phillips 2016-03-08 23:26:04 -08:00
commit e944b18d8b
3 changed files with 115 additions and 14 deletions

View File

@ -487,6 +487,25 @@ func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qTy
return records return records
} }
// trimAnswers makes sure a UDP response is not longer than allowed by RFC 1035.
// We first enforce an arbitrary limit, and then make sure the response doesn't
// exceed 512 bytes.
func trimAnswers(resp *dns.Msg) (trimmed bool) {
numAnswers := len(resp.Answer)
// This cuts UDP responses to a useful but limited number of responses.
if numAnswers > maxServiceResponses {
resp.Answer = resp.Answer[:maxServiceResponses]
}
// This enforces the hard limit of 512 bytes per the RFC.
for len(resp.Answer) > 0 && resp.Len() > 512 {
resp.Answer = resp.Answer[:len(resp.Answer)-1]
}
return len(resp.Answer) < numAnswers
}
// serviceLookup is used to handle a service query // serviceLookup is used to handle a service query
func (d *DNSServer) serviceLookup(network, datacenter, service, tag string, req, resp *dns.Msg) { func (d *DNSServer) serviceLookup(network, datacenter, service, tag string, req, resp *dns.Msg) {
// Make an RPC request // Make an RPC request
@ -547,17 +566,17 @@ RPC:
} }
// If the network is not TCP, restrict the number of responses // If the network is not TCP, restrict the number of responses
if network != "tcp" && len(resp.Answer) > maxServiceResponses { if network != "tcp" {
resp.Answer = resp.Answer[:maxServiceResponses] wasTrimmed := trimAnswers(resp)
// Flag that there are more records to return in the UDP response // Flag that there are more records to return in the UDP response
if d.config.EnableTruncate { if wasTrimmed && d.config.EnableTruncate {
resp.Truncated = true resp.Truncated = true
} }
} }
// If the answer is empty, return not found // If the answer is empty and the response isn't truncated, return not found
if len(resp.Answer) == 0 { if len(resp.Answer) == 0 && !resp.Truncated {
d.addSOA(d.domain, resp) d.addSOA(d.domain, resp)
return return
} }
@ -642,18 +661,17 @@ RPC:
} }
// If the network is not TCP, restrict the number of responses. // If the network is not TCP, restrict the number of responses.
if network != "tcp" && len(resp.Answer) > maxServiceResponses { if network != "tcp" {
resp.Answer = resp.Answer[:maxServiceResponses] wasTrimmed := trimAnswers(resp)
// Flag that there are more records to return in the UDP // Flag that there are more records to return in the UDP response
// response. if wasTrimmed && d.config.EnableTruncate {
if d.config.EnableTruncate {
resp.Truncated = true resp.Truncated = true
} }
} }
// If the answer is empty, return not found. // If the answer is empty and the response isn't truncated, return not found
if len(resp.Answer) == 0 { if len(resp.Answer) == 0 && !resp.Truncated {
d.addSOA(d.domain, resp) d.addSOA(d.domain, resp)
return return
} }

View File

@ -1961,6 +1961,88 @@ func TestDNS_ServiceLookup_Truncate(t *testing.T) {
} }
} }
func TestDNS_ServiceLookup_LargeResponses(t *testing.T) {
dir, srv := makeDNSServerConfig(t, nil, func(c *DNSConfig) {
c.EnableTruncate = true
})
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
longServiceName := "this-is-a-very-very-very-very-very-long-name-for-a-service"
// Register 3 nodes
for i := 0; i < 3; i++ {
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: fmt.Sprintf("foo%d", i),
Address: fmt.Sprintf("127.0.0.%d", i+1),
Service: &structs.NodeService{
Service: longServiceName,
Tags: []string{"master"},
Port: 12345,
},
}
var out struct{}
if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
// Register an equivalent prepared query.
{
args := &structs.PreparedQueryRequest{
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: longServiceName,
Service: structs.ServiceQuery{
Service: longServiceName,
Tags: []string{"master"},
},
},
}
var id string
if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil {
t.Fatalf("err: %v", err)
}
}
// Look up the service directly and via prepared query.
questions := []string{
"_" + longServiceName + "._master.service.consul.",
longServiceName + ".query.consul.",
}
for _, question := range questions {
m := new(dns.Msg)
m.SetQuestion(question, dns.TypeSRV)
c := new(dns.Client)
addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS)
in, _, err := c.Exchange(m, addr.String())
if err != nil && err != dns.ErrTruncated {
t.Fatalf("err: %v", err)
}
// Make sure the response size is RFC 1035-compliant for UDP messages
if in.Len() > 512 {
t.Fatalf("Bad: %#v", in.Len())
}
// We should only have two answers now
if len(in.Answer) != 2 {
t.Fatalf("Bad: %#v", len(in.Answer))
}
// Check for the truncate bit
if !in.Truncated {
t.Fatalf("should have truncate bit")
}
}
}
func TestDNS_ServiceLookup_MaxResponses(t *testing.T) { func TestDNS_ServiceLookup_MaxResponses(t *testing.T) {
dir, srv := makeDNSServer(t) dir, srv := makeDNSServer(t)
defer os.RemoveAll(dir) defer os.RemoveAll(dir)

View File

@ -493,8 +493,9 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
setting this value. setting this value.
* <a name="enable_truncate"></a><a href="#enable_truncate">`enable_truncate`</a> If set to * <a name="enable_truncate"></a><a href="#enable_truncate">`enable_truncate`</a> If set to
true, a UDP DNS query that would return more than 3 records will set the truncated flag, true, a UDP DNS query that would return more 3 records, or more than would fit into a valid
indicating to clients that they should re-query using TCP to get the full set of records. UDP response, will set the truncated flag, indicating to clients that they should re-query
using TCP to get the full set of records.
* <a name="only_passing"></a><a href="#only_passing">`only_passing`</a> If set to true, any * <a name="only_passing"></a><a href="#only_passing">`only_passing`</a> If set to true, any
nodes whose healthchecks are not passing will be excluded from DNS results. By default (or nodes whose healthchecks are not passing will be excluded from DNS results. By default (or