feat: caching for empty DNS responses (#700)

This commit is contained in:
Dimitri Herzog 2022-11-08 10:27:59 +00:00
parent 2037e862dc
commit 3e95b12eed
8 changed files with 125 additions and 63 deletions

View File

@ -23,6 +23,8 @@ const (
IPVersionV6
)
var ErrInvalidIPVersion = fmt.Errorf("not a valid IPVersion, try [%s]", strings.Join(_IPVersionNames, ", "))
const _IPVersionName = "dualv4v6"
var _IPVersionNames = []string{
@ -63,7 +65,7 @@ func ParseIPVersion(name string) (IPVersion, error) {
if x, ok := _IPVersionValue[name]; ok {
return x, nil
}
return IPVersion(0), fmt.Errorf("%s is not a valid IPVersion, try [%s]", name, strings.Join(_IPVersionNames, ", "))
return IPVersion(0), fmt.Errorf("%s is %w", name, ErrInvalidIPVersion)
}
// MarshalText implements the text marshaller method.
@ -94,6 +96,8 @@ const (
NetProtocolHttps
)
var ErrInvalidNetProtocol = fmt.Errorf("not a valid NetProtocol, try [%s]", strings.Join(_NetProtocolNames, ", "))
const _NetProtocolName = "tcp+udptcp-tlshttps"
var _NetProtocolNames = []string{
@ -134,7 +138,7 @@ func ParseNetProtocol(name string) (NetProtocol, error) {
if x, ok := _NetProtocolValue[name]; ok {
return x, nil
}
return NetProtocol(0), fmt.Errorf("%s is not a valid NetProtocol, try [%s]", name, strings.Join(_NetProtocolNames, ", "))
return NetProtocol(0), fmt.Errorf("%s is %w", name, ErrInvalidNetProtocol)
}
// MarshalText implements the text marshaller method.
@ -174,6 +178,8 @@ const (
QueryLogTypeCsvClient
)
var ErrInvalidQueryLogType = fmt.Errorf("not a valid QueryLogType, try [%s]", strings.Join(_QueryLogTypeNames, ", "))
const _QueryLogTypeName = "consolenonemysqlpostgresqlcsvcsv-client"
var _QueryLogTypeNames = []string{
@ -223,7 +229,7 @@ func ParseQueryLogType(name string) (QueryLogType, error) {
if x, ok := _QueryLogTypeValue[name]; ok {
return x, nil
}
return QueryLogType(0), fmt.Errorf("%s is not a valid QueryLogType, try [%s]", name, strings.Join(_QueryLogTypeNames, ", "))
return QueryLogType(0), fmt.Errorf("%s is %w", name, ErrInvalidQueryLogType)
}
// MarshalText implements the text marshaller method.
@ -254,6 +260,8 @@ const (
StartStrategyTypeFast
)
var ErrInvalidStartStrategyType = fmt.Errorf("not a valid StartStrategyType, try [%s]", strings.Join(_StartStrategyTypeNames, ", "))
const _StartStrategyTypeName = "blockingfailOnErrorfast"
var _StartStrategyTypeNames = []string{
@ -294,7 +302,7 @@ func ParseStartStrategyType(name string) (StartStrategyType, error) {
if x, ok := _StartStrategyTypeValue[name]; ok {
return x, nil
}
return StartStrategyType(0), fmt.Errorf("%s is not a valid StartStrategyType, try [%s]", name, strings.Join(_StartStrategyTypeNames, ", "))
return StartStrategyType(0), fmt.Errorf("%s is %w", name, ErrInvalidStartStrategyType)
}
// MarshalText implements the text marshaller method.

View File

@ -140,6 +140,9 @@ caching:
# Max number of domains to be kept in cache for prefetching (soft limit). Useful on systems with limited amount of RAM.
# Default (0): unlimited
prefetchMaxItemsCount: 0
# Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results.
# Default: 30m
cacheTimeNegative: 30m
# optional: configuration of client name resolution
clientLookup:

View File

@ -514,7 +514,7 @@ With following parameters you can tune the caching behavior:
| caching.prefetchExpires | duration format | no | 2h | Prefetch track time window |
| caching.prefetchThreshold | int | no | 5 | Name queries threshold for prefetch |
| caching.prefetchMaxItemsCount | int | no | 0 (unlimited) | Max number of domains to be kept in cache for prefetching (soft limit). Default (0): unlimited. Useful on systems with limited amount of RAM. |
| caching.cacheTimeNegative | duration format | no | 30m | Time how long negative results are cached. A value of -1 will disable caching for negative results. |
| caching.cacheTimeNegative | duration format | no | 30m | Time how long negative results (NXDOMAIN response or empty result) are cached. A value of -1 will disable caching for negative results. |
!!! example

View File

@ -20,6 +20,8 @@ const (
ListCacheTypeWhitelist
)
var ErrInvalidListCacheType = fmt.Errorf("not a valid ListCacheType, try [%s]", strings.Join(_ListCacheTypeNames, ", "))
const _ListCacheTypeName = "blacklistwhitelist"
var _ListCacheTypeNames = []string{
@ -57,7 +59,7 @@ func ParseListCacheType(name string) (ListCacheType, error) {
if x, ok := _ListCacheTypeValue[name]; ok {
return x, nil
}
return ListCacheType(0), fmt.Errorf("%s is not a valid ListCacheType, try [%s]", name, strings.Join(_ListCacheTypeNames, ", "))
return ListCacheType(0), fmt.Errorf("%s is %w", name, ErrInvalidListCacheType)
}
// MarshalText implements the text marshaller method.

View File

@ -20,6 +20,8 @@ const (
FormatTypeJson
)
var ErrInvalidFormatType = fmt.Errorf("not a valid FormatType, try [%s]", strings.Join(_FormatTypeNames, ", "))
const _FormatTypeName = "textjson"
var _FormatTypeNames = []string{
@ -57,7 +59,7 @@ func ParseFormatType(name string) (FormatType, error) {
if x, ok := _FormatTypeValue[name]; ok {
return x, nil
}
return FormatType(0), fmt.Errorf("%s is not a valid FormatType, try [%s]", name, strings.Join(_FormatTypeNames, ", "))
return FormatType(0), fmt.Errorf("%s is %w", name, ErrInvalidFormatType)
}
// MarshalText implements the text marshaller method.
@ -91,6 +93,8 @@ const (
LevelFatal
)
var ErrInvalidLevel = fmt.Errorf("not a valid Level, try [%s]", strings.Join(_LevelNames, ", "))
const _LevelName = "infotracedebugwarnerrorfatal"
var _LevelNames = []string{
@ -140,7 +144,7 @@ func ParseLevel(name string) (Level, error) {
if x, ok := _LevelValue[name]; ok {
return x, nil
}
return Level(0), fmt.Errorf("%s is not a valid Level, try [%s]", name, strings.Join(_LevelNames, ", "))
return Level(0), fmt.Errorf("%s is %w", name, ErrInvalidLevel)
}
// MarshalText implements the text marshaller method.

View File

@ -20,6 +20,8 @@ const (
RequestProtocolUDP
)
var ErrInvalidRequestProtocol = fmt.Errorf("not a valid RequestProtocol, try [%s]", strings.Join(_RequestProtocolNames, ", "))
const _RequestProtocolName = "TCPUDP"
var _RequestProtocolNames = []string{
@ -57,7 +59,7 @@ func ParseRequestProtocol(name string) (RequestProtocol, error) {
if x, ok := _RequestProtocolValue[name]; ok {
return x, nil
}
return RequestProtocol(0), fmt.Errorf("%s is not a valid RequestProtocol, try [%s]", name, strings.Join(_RequestProtocolNames, ", "))
return RequestProtocol(0), fmt.Errorf("%s is %w", name, ErrInvalidRequestProtocol)
}
// MarshalText implements the text marshaller method.
@ -106,6 +108,8 @@ const (
ResponseTypeSPECIAL
)
var ErrInvalidResponseType = fmt.Errorf("not a valid ResponseType, try [%s]", strings.Join(_ResponseTypeNames, ", "))
const _ResponseTypeName = "RESOLVEDCACHEDBLOCKEDCONDITIONALCUSTOMDNSHOSTSFILEFILTEREDNOTFQDNSPECIAL"
var _ResponseTypeNames = []string{
@ -164,7 +168,7 @@ func ParseResponseType(name string) (ResponseType, error) {
if x, ok := _ResponseTypeValue[name]; ok {
return x, nil
}
return ResponseType(0), fmt.Errorf("%s is not a valid ResponseType, try [%s]", name, strings.Join(_ResponseTypeNames, ", "))
return ResponseType(0), fmt.Errorf("%s is %w", name, ErrInvalidResponseType)
}
// MarshalText implements the text marshaller method.

View File

@ -112,7 +112,7 @@ func (r *CachingResolver) onExpired(cacheKey string) (val interface{}, ttl time.
if response.Res.Rcode == dns.RcodeSuccess {
evt.Bus().Publish(evt.CachingDomainPrefetched, domainName)
return cacheValue{response.Res.Answer, true}, time.Duration(r.adjustTTLs(response.Res.Answer)) * time.Second
return cacheValue{response.Res.Answer, true}, r.adjustTTLs(response.Res.Answer)
}
} else {
util.LogOnError(fmt.Sprintf("can't prefetch '%s' ", domainName), err)
@ -232,7 +232,7 @@ func (r *CachingResolver) putInCache(cacheKey string, response *model.Response,
if response.Res.Rcode == dns.RcodeSuccess {
// put value into cache
r.resultCache.Put(cacheKey, cacheValue{answer, prefetch}, time.Duration(r.adjustTTLs(answer))*time.Second)
r.resultCache.Put(cacheKey, cacheValue{answer, prefetch}, r.adjustTTLs(answer))
} else if response.Res.Rcode == dns.RcodeNameError {
if r.cacheTimeNegative > 0 {
// put return code if NXDOMAIN
@ -249,7 +249,16 @@ func (r *CachingResolver) putInCache(cacheKey string, response *model.Response,
}
}
func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) {
// adjustTTLs calculates and returns the max TTL (considers also the min and max cache time)
// for all records from answer or a negative cache time for empty answer
// adjust the TTL in the answer header accordingly
func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL time.Duration) {
var max uint32
if len(answer) == 0 {
return r.cacheTimeNegative
}
for _, a := range answer {
// if TTL < mitTTL -> adjust the value, set minTTL
if r.minCacheTimeSec > 0 {
@ -264,10 +273,10 @@ func (r *CachingResolver) adjustTTLs(answer []dns.RR) (maxTTL uint32) {
}
}
if maxTTL < a.Header().Ttl {
maxTTL = a.Header().Ttl
if max < a.Header().Ttl {
max = a.Header().Ttl
}
}
return
return time.Duration(max) * time.Second
}

View File

@ -355,66 +355,98 @@ var _ = Describe("CachingResolver", func() {
})
Describe("Negative cache (caching if upstream resolver returns NXDOMAIN)", func() {
When("Upstream resolver returns NXDOMAIN with caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
})
It("response should be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
Context("Caching if upstream resolver returns NXDOMAIN", func() {
When("Upstream resolver returns NXDOMAIN with caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
})
By("second request", func() {
Eventually(func(g Gomega) {
It("response should be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED))
g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE"))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
// still one call to resolver
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed())
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
})
By("second request", func() {
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED))
g.Expect(resp.Reason).Should(Equal("CACHED NEGATIVE"))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
// still one call to resolver
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed())
})
})
})
When("Upstream resolver returns NXDOMAIN without caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
sutConfig = config.CachingConfig{
CacheTimeNegative: config.Duration(time.Minute * -1),
}
})
It("response shouldn't be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
})
By("second request", func() {
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
g.Expect(m.Calls).Should(HaveLen(2))
}, "500ms").Should(Succeed())
})
})
})
})
When("Upstream resolver returns NXDOMAIN without caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeNameError
sutConfig = config.CachingConfig{
CacheTimeNegative: config.Duration(time.Minute * -1),
}
})
It("response shouldn't be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
Expect(m.Calls).Should(HaveLen(1))
Context("Caching if upstream resolver returns empty result", func() {
When("Upstream resolver returns empty result with caching", func() {
BeforeEach(func() {
mockAnswer.Rcode = dns.RcodeSuccess
mockAnswer.Answer = make([]dns.RR, 0)
})
By("second request", func() {
Eventually(func(g Gomega) {
It("response should be cached", func() {
By("first request", func() {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeNameError))
g.Expect(m.Calls).Should(HaveLen(2))
}, "500ms").Should(Succeed())
})
})
Expect(err).Should(Succeed())
Expect(resp.RType).Should(Equal(ResponseTypeRESOLVED))
Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
Expect(m.Calls).Should(HaveLen(1))
})
By("second request", func() {
Eventually(func(g Gomega) {
resp, err = sut.Resolve(newRequest("example.com.", dns.Type(dns.TypeAAAA)))
g.Expect(err).Should(Succeed())
g.Expect(resp.RType).Should(Equal(ResponseTypeCACHED))
g.Expect(resp.Reason).Should(Equal("CACHED"))
g.Expect(resp.Res.Rcode).Should(Equal(dns.RcodeSuccess))
// still one call to resolver
g.Expect(m.Calls).Should(HaveLen(1))
}, "500ms").Should(Succeed())
})
})
})
})
})
Describe("Not A / AAAA queries should also cached", func() {
Describe("Not A / AAAA queries should also be cached", func() {
When("MX query will be performed", func() {
BeforeEach(func() {
mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, dns.Type(dns.TypeMX), "10 alt1.aspmx.l.google.com.")