mirror of https://github.com/hashicorp/consul
Browse Source
* no-op commit due to failed cherry-picking * DNS v2 Multiple fixes. (#20525) * DNS v2 Multiple fixes. * add license header * get rid of DefaultIntentionPolicy change that was not supposed to be there. --------- Co-authored-by: temp <temp@hashicorp.com> Co-authored-by: John Murret <john.murret@hashicorp.com>pull/20534/head
hc-github-team-consul-core
10 months ago
committed by
GitHub
11 changed files with 438 additions and 118 deletions
@ -0,0 +1,259 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
package dns |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/hashicorp/consul/lib" |
||||
"github.com/hashicorp/go-hclog" |
||||
"github.com/miekg/dns" |
||||
"math" |
||||
"strings" |
||||
) |
||||
|
||||
const ( |
||||
// UDP can fit ~25 A records in a 512B response, and ~14 AAAA
|
||||
// records. Limit further to prevent unintentional configuration
|
||||
// abuse that would have a negative effect on application response
|
||||
// times.
|
||||
maxUDPAnswerLimit = 8 |
||||
|
||||
defaultMaxUDPSize = 512 |
||||
|
||||
// If a consumer sets a buffer size greater than this amount we will default it down
|
||||
// to this amount to ensure that consul does respond. Previously if consumer had a larger buffer
|
||||
// size than 65535 - 60 bytes (maximim 60 bytes for IP header. UDP header will be offset in the
|
||||
// trimUDP call) consul would fail to respond and the consumer timesout
|
||||
// the request.
|
||||
maxUDPDatagramSize = math.MaxUint16 - 68 |
||||
) |
||||
|
||||
// trimDNSResponse will trim the response for UDP and TCP
|
||||
func trimDNSResponse(cfg *RouterDynamicConfig, network string, req, resp *dns.Msg, logger hclog.Logger) { |
||||
var trimmed bool |
||||
originalSize := resp.Len() |
||||
originalNumRecords := len(resp.Answer) |
||||
if network != "tcp" { |
||||
trimmed = trimUDPResponse(req, resp, cfg.UDPAnswerLimit) |
||||
} else { |
||||
trimmed = trimTCPResponse(req, resp) |
||||
} |
||||
// Flag that there are more records to return in the UDP response
|
||||
if trimmed { |
||||
if cfg.EnableTruncate { |
||||
resp.Truncated = true |
||||
} |
||||
logger.Debug("DNS response too large, truncated", |
||||
"protocol", network, |
||||
"question", req.Question, |
||||
"records", fmt.Sprintf("%d/%d", len(resp.Answer), originalNumRecords), |
||||
"size", fmt.Sprintf("%d/%d", resp.Len(), originalSize), |
||||
) |
||||
} |
||||
} |
||||
|
||||
// trimTCPResponse limit the MaximumSize of messages to 64k as it is the limit
|
||||
// of DNS responses
|
||||
func trimTCPResponse(req, resp *dns.Msg) (trimmed bool) { |
||||
hasExtra := len(resp.Extra) > 0 |
||||
// There is some overhead, 65535 does not work
|
||||
maxSize := 65523 // 64k - 12 bytes DNS raw overhead
|
||||
|
||||
// We avoid some function calls and allocations by only handling the
|
||||
// extra data when necessary.
|
||||
var index map[string]dns.RR |
||||
|
||||
// It is not possible to return more than 4k records even with compression
|
||||
// Since we are performing binary search it is not a big deal, but it
|
||||
// improves a bit performance, even with binary search
|
||||
truncateAt := 4096 |
||||
if req.Question[0].Qtype == dns.TypeSRV { |
||||
// More than 1024 SRV records do not fit in 64k
|
||||
truncateAt = 1024 |
||||
} |
||||
if len(resp.Answer) > truncateAt { |
||||
resp.Answer = resp.Answer[:truncateAt] |
||||
} |
||||
if hasExtra { |
||||
index = make(map[string]dns.RR, len(resp.Extra)) |
||||
indexRRs(resp.Extra, index) |
||||
} |
||||
truncated := false |
||||
|
||||
// This enforces the given limit on 64k, the max limit for DNS messages
|
||||
for len(resp.Answer) > 1 && resp.Len() > maxSize { |
||||
truncated = true |
||||
// first try to remove the NS section may be it will truncate enough
|
||||
if len(resp.Ns) != 0 { |
||||
resp.Ns = []dns.RR{} |
||||
} |
||||
// More than 100 bytes, find with a binary search
|
||||
if resp.Len()-maxSize > 100 { |
||||
bestIndex := dnsBinaryTruncate(resp, maxSize, index, hasExtra) |
||||
resp.Answer = resp.Answer[:bestIndex] |
||||
} else { |
||||
resp.Answer = resp.Answer[:len(resp.Answer)-1] |
||||
} |
||||
if hasExtra { |
||||
syncExtra(index, resp) |
||||
} |
||||
} |
||||
|
||||
return truncated |
||||
} |
||||
|
||||
// trimUDPResponse makes sure a UDP response is not longer than allowed by RFC
|
||||
// 1035. Enforce an arbitrary limit that can be further ratcheted down by
|
||||
// config, and then make sure the response doesn't exceed 512 bytes. Any extra
|
||||
// records will be trimmed along with answers.
|
||||
func trimUDPResponse(req, resp *dns.Msg, udpAnswerLimit int) (trimmed bool) { |
||||
numAnswers := len(resp.Answer) |
||||
hasExtra := len(resp.Extra) > 0 |
||||
maxSize := defaultMaxUDPSize |
||||
|
||||
// Update to the maximum edns size
|
||||
if edns := req.IsEdns0(); edns != nil { |
||||
if size := edns.UDPSize(); size > uint16(maxSize) { |
||||
maxSize = int(size) |
||||
} |
||||
} |
||||
// Overriding maxSize as the maxSize cannot be larger than the
|
||||
// maxUDPDatagram size. Reliability guarantees disappear > than this amount.
|
||||
if maxSize > maxUDPDatagramSize { |
||||
maxSize = maxUDPDatagramSize |
||||
} |
||||
|
||||
// We avoid some function calls and allocations by only handling the
|
||||
// extra data when necessary.
|
||||
var index map[string]dns.RR |
||||
if hasExtra { |
||||
index = make(map[string]dns.RR, len(resp.Extra)) |
||||
indexRRs(resp.Extra, index) |
||||
} |
||||
|
||||
// This cuts UDP responses to a useful but limited number of responses.
|
||||
maxAnswers := lib.MinInt(maxUDPAnswerLimit, udpAnswerLimit) |
||||
compress := resp.Compress |
||||
if maxSize == defaultMaxUDPSize && numAnswers > maxAnswers { |
||||
// We disable computation of Len ONLY for non-eDNS request (512 bytes)
|
||||
resp.Compress = false |
||||
resp.Answer = resp.Answer[:maxAnswers] |
||||
if hasExtra { |
||||
syncExtra(index, resp) |
||||
} |
||||
} |
||||
if maxSize == defaultMaxUDPSize && numAnswers > maxAnswers { |
||||
// We disable computation of Len ONLY for non-eDNS request (512 bytes)
|
||||
resp.Compress = false |
||||
resp.Answer = resp.Answer[:maxAnswers] |
||||
if hasExtra { |
||||
syncExtra(index, resp) |
||||
} |
||||
} |
||||
|
||||
// This enforces the given limit on the number bytes. The default is 512 as
|
||||
// per the RFC, but EDNS0 allows for the user to specify larger sizes. Note
|
||||
// that we temporarily switch to uncompressed so that we limit to a response
|
||||
// that will not exceed 512 bytes uncompressed, which is more conservative and
|
||||
// will allow our responses to be compliant even if some downstream server
|
||||
// uncompresses them.
|
||||
// Even when size is too big for one single record, try to send it anyway
|
||||
// (useful for 512 bytes messages). 8 is removed from maxSize to ensure that we account
|
||||
// for the udp header (8 bytes).
|
||||
for len(resp.Answer) > 1 && resp.Len() > maxSize-8 { |
||||
// first try to remove the NS section may be it will truncate enough
|
||||
if len(resp.Ns) != 0 { |
||||
resp.Ns = []dns.RR{} |
||||
} |
||||
// More than 100 bytes, find with a binary search
|
||||
if resp.Len()-maxSize > 100 { |
||||
bestIndex := dnsBinaryTruncate(resp, maxSize, index, hasExtra) |
||||
resp.Answer = resp.Answer[:bestIndex] |
||||
} else { |
||||
resp.Answer = resp.Answer[:len(resp.Answer)-1] |
||||
} |
||||
if hasExtra { |
||||
syncExtra(index, resp) |
||||
} |
||||
} |
||||
// For 512 non-eDNS responses, while we compute size non-compressed,
|
||||
// we send result compressed
|
||||
resp.Compress = compress |
||||
return len(resp.Answer) < numAnswers |
||||
} |
||||
|
||||
// syncExtra takes a DNS response message and sets the extra data to the most
|
||||
// minimal set needed to cover the answer data. A pre-made index of RRs is given
|
||||
// so that can be re-used between calls. This assumes that the extra data is
|
||||
// only used to provide info for SRV records. If that's not the case, then this
|
||||
// will wipe out any additional data.
|
||||
func syncExtra(index map[string]dns.RR, resp *dns.Msg) { |
||||
extra := make([]dns.RR, 0, len(resp.Answer)) |
||||
resolved := make(map[string]struct{}, len(resp.Answer)) |
||||
for _, ansRR := range resp.Answer { |
||||
srv, ok := ansRR.(*dns.SRV) |
||||
if !ok { |
||||
continue |
||||
} |
||||
|
||||
// Note that we always use lower case when using the index so
|
||||
// that compares are not case-sensitive. We don't alter the actual
|
||||
// RRs we add into the extra section, however.
|
||||
target := strings.ToLower(srv.Target) |
||||
|
||||
RESOLVE: |
||||
if _, ok := resolved[target]; ok { |
||||
continue |
||||
} |
||||
resolved[target] = struct{}{} |
||||
|
||||
extraRR, ok := index[target] |
||||
if ok { |
||||
extra = append(extra, extraRR) |
||||
if cname, ok := extraRR.(*dns.CNAME); ok { |
||||
target = strings.ToLower(cname.Target) |
||||
goto RESOLVE |
||||
} |
||||
} |
||||
} |
||||
resp.Extra = extra |
||||
} |
||||
|
||||
// dnsBinaryTruncate find the optimal number of records using a fast binary search and return
|
||||
// it in order to return a DNS answer lower than maxSize parameter.
|
||||
func dnsBinaryTruncate(resp *dns.Msg, maxSize int, index map[string]dns.RR, hasExtra bool) int { |
||||
originalAnswser := resp.Answer |
||||
startIndex := 0 |
||||
endIndex := len(resp.Answer) + 1 |
||||
for endIndex-startIndex > 1 { |
||||
median := startIndex + (endIndex-startIndex)/2 |
||||
|
||||
resp.Answer = originalAnswser[:median] |
||||
if hasExtra { |
||||
syncExtra(index, resp) |
||||
} |
||||
aLen := resp.Len() |
||||
if aLen <= maxSize { |
||||
if maxSize-aLen < 10 { |
||||
// We are good, increasing will go out of bounds
|
||||
return median |
||||
} |
||||
startIndex = median |
||||
} else { |
||||
endIndex = median |
||||
} |
||||
} |
||||
return startIndex |
||||
} |
||||
|
||||
// indexRRs populates a map which indexes a given list of RRs by name. NOTE that
|
||||
// the names are all squashed to lower case so we can perform case-insensitive
|
||||
// lookups; the RRs are not modified.
|
||||
func indexRRs(rrs []dns.RR, index map[string]dns.RR) { |
||||
for _, rr := range rrs { |
||||
name := strings.ToLower(rr.Header().Name) |
||||
if _, ok := index[name]; !ok { |
||||
index[name] = rr |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue