Skip to content

Commit

Permalink
New cache-harden-below-nxdomain option (folbricht#132)
Browse files Browse the repository at this point in the history
* New cache-harden-below-nxdomain option

* Wire in the config
  • Loading branch information
folbricht authored Mar 2, 2021
1 parent 31a59c1 commit 7fdf2fa
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 7 deletions.
27 changes: 27 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"expvar"
"math"
"math/rand"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -48,6 +49,12 @@ type CacheOptions struct {
// Allows control over the order of answer RRs in cached responses. Default is to keep
// the order if nil.
ShuffleAnswerFunc AnswerShuffleFunc

// If enabled, will return NXDOMAIN for every name query under another name that is
// already cached as NXDOMAIN. For example, if example.com is in the cache with
// NXDOMAIN, a query for www.example.com will also immediately return NXDOMAIN.
// See RFC8020.
HardenBelowNXDOMAIN bool
}

// NewCache returns a new instance of a Cache resolver.
Expand Down Expand Up @@ -128,6 +135,26 @@ func (r *Cache) answerFromCache(q *dns.Msg) (*dns.Msg, bool) {
}
r.mu.Unlock()

// We couldn't find it in the cache, but a parent domain may already be with NXDOMAIN.
// Return that instead if enabled.
if answer == nil && r.HardenBelowNXDOMAIN {
name := q.Question[0].Name
newQ := q.Copy()
fragments := strings.Split(name, ".")
r.mu.Lock()
for i := 1; i < len(fragments)-1; i++ {
newQ.Question[0].Name = strings.Join(fragments[i:], ".")
if a := r.lru.get(newQ); a != nil {
if a.Rcode == dns.RcodeNameError {
r.mu.Unlock()
return nxdomain(q), true
}
break
}
}
r.mu.Unlock()
}

// Return a cache-miss if there's no answer record in the map
if answer == nil {
return nil, false
Expand Down
33 changes: 33 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,39 @@ func TestCacheNXDOMAIN(t *testing.T) {
require.Equal(t, 1, r.HitCount())
}

func TestCacheHardenBelowNXDOMAIN(t *testing.T) {
var ci ClientInfo
q := new(dns.Msg)
r := &TestResolver{
ResolveFunc: func(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
a := new(dns.Msg)
a.SetReply(q)
a.SetRcode(q, dns.RcodeNameError)
return a, nil
},
}

opt := CacheOptions{
GCPeriod: time.Minute,
HardenBelowNXDOMAIN: true,
}
c := NewCache("test-cache", r, opt)

// Cache an NXDOMAIN for the parent domain
q.SetQuestion("test.com.", dns.TypeA)
_, err := c.Resolve(q, ci)
require.NoError(t, err)
require.Equal(t, 1, r.HitCount())

// A sub-domain query should also return NXDOMAIN based on the cached
// record for the parent if HardenBelowNXDOMAIN is enabled.
q.SetQuestion("not.exist.test.com.", dns.TypeA)
a, err := c.Resolve(q, ci)
require.NoError(t, err)
require.Equal(t, 1, r.HitCount())
require.Equal(t, dns.RcodeNameError, a.Rcode)
}

func TestRoundRobinShuffle(t *testing.T) {
msg := &dns.Msg{
Answer: []dns.RR{
Expand Down
7 changes: 4 additions & 3 deletions cmd/routedns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ type group struct {
EDNS0Data []byte `toml:"edns0-data"` // EDNS0 modifier option data

// Cache options
CacheSize int `toml:"cache-size"` // Max number of items to keep in the cache. Default 0 == unlimited
CacheNegativeTTL uint32 `toml:"cache-negative-ttl"` // TTL to apply to negative responses, default 60.
CacheAnswerShuffle string `toml:"cache-answer-shuffle"` // Algorithm to use for modifying the response order of cached items
CacheSize int `toml:"cache-size"` // Max number of items to keep in the cache. Default 0 == unlimited
CacheNegativeTTL uint32 `toml:"cache-negative-ttl"` // TTL to apply to negative responses, default 60.
CacheAnswerShuffle string `toml:"cache-answer-shuffle"` // Algorithm to use for modifying the response order of cached items
CacheHardenBelowNXDOMAIN bool `toml:"cache-harden-below-nxdomain"` // Return NXDOMAIN if an NXDOMAIN is cached for a parent domain

// Blocklist options
Blocklist []string // Blocklist rules, only used by "blocklist" type
Expand Down
9 changes: 5 additions & 4 deletions cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,11 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
return fmt.Errorf("unsupported shuffle function %q", g.CacheAnswerShuffle)
}
opt := rdns.CacheOptions{
GCPeriod: time.Duration(g.GCPeriod) * time.Second,
Capacity: g.CacheSize,
NegativeTTL: g.CacheNegativeTTL,
ShuffleAnswerFunc: shuffleFunc,
GCPeriod: time.Duration(g.GCPeriod) * time.Second,
Capacity: g.CacheSize,
NegativeTTL: g.CacheNegativeTTL,
ShuffleAnswerFunc: shuffleFunc,
HardenBelowNXDOMAIN: g.CacheHardenBelowNXDOMAIN,
}
resolvers[id] = rdns.NewCache(id, gr[0], opt)
case "response-blocklist-ip", "response-blocklist-cidr": // "response-blocklist-cidr" has been retired/renamed to "response-blocklist-ip"
Expand Down
1 change: 1 addition & 0 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ Options:
- `cache-size` - Max number of responses to cache. Defaults to 0 which means no limit. Optional
- `cache-negative-ttl` - TTL (in seconds) to apply to responses without a SOA. Default: 60. Optional
- `cache-answer-shuffle` - Specifies a method for changing the order of cached A/AAAA answer records. Possible values `random` or `round-robin`. Defaults to static responses if not set.
- `cache-harden-below-nxdomain` - Return NXDOMAIN for sudomain queries if the parent domain has a cached NXDOMAIN. See [RFC8020](https://tools.ietf.org/html/rfc8020).

#### Examples

Expand Down

0 comments on commit 7fdf2fa

Please sign in to comment.