mirror of https://github.com/0xERR0R/blocky.git
842 lines
24 KiB
Go
842 lines
24 KiB
Go
package resolver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/0xERR0R/blocky/config"
|
|
. "github.com/0xERR0R/blocky/evt"
|
|
. "github.com/0xERR0R/blocky/helpertest"
|
|
"github.com/0xERR0R/blocky/log"
|
|
. "github.com/0xERR0R/blocky/model"
|
|
"github.com/0xERR0R/blocky/redis"
|
|
"github.com/0xERR0R/blocky/util"
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/creasty/defaults"
|
|
|
|
"github.com/miekg/dns"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
var _ = Describe("CachingResolver", func() {
|
|
var (
|
|
sut *CachingResolver
|
|
sutConfig config.CachingConfig
|
|
m *mockResolver
|
|
mockAnswer *dns.Msg
|
|
ctx context.Context
|
|
cancelFn context.CancelFunc
|
|
)
|
|
|
|
Describe("Type", func() {
|
|
It("follows conventions", func() {
|
|
expectValidResolverType(sut)
|
|
})
|
|
})
|
|
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{}
|
|
if err := defaults.Set(&sutConfig); err != nil {
|
|
panic(err)
|
|
}
|
|
mockAnswer = new(dns.Msg)
|
|
})
|
|
|
|
JustBeforeEach(func() {
|
|
ctx, cancelFn = context.WithCancel(context.Background())
|
|
DeferCleanup(cancelFn)
|
|
|
|
sut = NewCachingResolver(ctx, sutConfig, nil)
|
|
m = &mockResolver{}
|
|
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer}, nil)
|
|
sut.Next(m)
|
|
})
|
|
|
|
Describe("IsEnabled", func() {
|
|
It("is true", func() {
|
|
Expect(sut.IsEnabled()).Should(BeTrue())
|
|
})
|
|
|
|
When("max caching time is negative", func() {
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
MaxCachingTime: config.Duration(time.Minute * -1),
|
|
}
|
|
})
|
|
It("is false", func() {
|
|
Expect(sut.IsEnabled()).Should(BeFalse())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("LogConfig", func() {
|
|
It("should log something", func() {
|
|
logger, hook := log.NewMockEntry()
|
|
|
|
sut.LogConfig(logger)
|
|
|
|
Expect(hook.Calls).ShouldNot(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Describe("Caching responses", func() {
|
|
When("prefetching is enabled", func() {
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
Prefetching: true,
|
|
PrefetchExpires: config.Duration(time.Minute * 120),
|
|
PrefetchThreshold: 5,
|
|
}
|
|
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 2, A, "123.122.121.120")
|
|
})
|
|
|
|
It("should prefetch domain if query count > threshold", func() {
|
|
// prepare resolver, set smaller caching times for testing
|
|
prefetchThreshold := 5
|
|
configureCaches(ctx, sut, &sutConfig)
|
|
|
|
domainPrefetched := make(chan bool, 1)
|
|
prefetchHitDomain := make(chan bool, 1)
|
|
prefetchedCnt := make(chan int, 1)
|
|
Expect(Bus().SubscribeOnce(CachingPrefetchCacheHit, func(domain string) {
|
|
prefetchHitDomain <- true
|
|
})).Should(Succeed())
|
|
Expect(Bus().SubscribeOnce(CachingDomainPrefetched, func(domain string) {
|
|
domainPrefetched <- true
|
|
})).Should(Succeed())
|
|
|
|
Expect(Bus().SubscribeOnce(CachingDomainsToPrefetchCountChanged, func(cnt int) {
|
|
prefetchedCnt <- cnt
|
|
})).Should(Succeed())
|
|
|
|
// first request
|
|
_, _ = sut.Resolve(ctx, newRequest("example.com.", A))
|
|
|
|
// Domain is not prefetched
|
|
Expect(domainPrefetched).ShouldNot(Receive())
|
|
|
|
// Domain is in prefetched domain cache
|
|
Expect(prefetchedCnt).Should(Receive(Equal(1)))
|
|
|
|
// now query again > threshold
|
|
for i := 0; i < prefetchThreshold+1; i++ {
|
|
_, err := sut.Resolve(ctx, newRequest("example.com.", A))
|
|
Expect(err).Should(Succeed())
|
|
}
|
|
|
|
// now is this domain prefetched
|
|
Eventually(domainPrefetched, "10s").Should(Receive(Equal(true)))
|
|
|
|
// and it should hit from prefetch cache
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", A, "123.122.121.120"),
|
|
HaveTTL(BeNumerically("<=", 2))))
|
|
Eventually(prefetchHitDomain, "10s").Should(Receive(Equal(true)))
|
|
})
|
|
})
|
|
When("caching with default values is enabled", func() {
|
|
BeforeEach(func() {
|
|
rr1, err := dns.NewRR(fmt.Sprintf("%s\t%d\tIN\t%s\t%s", "example.com.", 600, A, "1.2.3.4"))
|
|
Expect(err).Should(Succeed())
|
|
|
|
rr2, err := dns.NewRR(fmt.Sprintf("%s\t%d\tIN\t%s\t%s", "example.com.", 950, CNAME, "cname.example.com"))
|
|
Expect(err).Should(Succeed())
|
|
|
|
msg := new(dns.Msg)
|
|
msg.Answer = []dns.RR{rr1, rr2}
|
|
mockAnswer = msg
|
|
})
|
|
It("should cache response and use response's TTL for multiple records", func() {
|
|
By("first request", func() {
|
|
result, err := sut.Resolve(ctx, newRequest("example.com.", A))
|
|
Expect(err).Should(Succeed())
|
|
Expect(result).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
WithTransform(ToAnswer, SatisfyAll(
|
|
HaveLen(2),
|
|
)),
|
|
))
|
|
|
|
Expect(result.Res.Answer[0]).Should(HaveTTL(BeNumerically("==", 600)))
|
|
Expect(result.Res.Answer[1]).Should(HaveTTL(BeNumerically("==", 950)))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(func(g Gomega) {
|
|
result, err := sut.Resolve(ctx, newRequest("example.com.", A))
|
|
g.Expect(err).Should(Succeed())
|
|
g.Expect(result).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
WithTransform(ToAnswer, SatisfyAll(
|
|
HaveLen(2),
|
|
))))
|
|
|
|
g.Expect(result.Res.Answer[0]).Should(HaveTTL(BeNumerically("<=", 599)))
|
|
g.Expect(result.Res.Answer[1]).Should(HaveTTL(BeNumerically("<=", 949)))
|
|
|
|
// still one call to upstream
|
|
g.Expect(m.Calls).Should(HaveLen(1))
|
|
}, "1s").Should(Succeed())
|
|
})
|
|
})
|
|
})
|
|
When("min caching time is defined", func() {
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
MinCachingTime: config.Duration(time.Minute * 5),
|
|
}
|
|
})
|
|
Context("response TTL is bigger than defined min caching time", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 600, A, "123.122.121.120")
|
|
})
|
|
|
|
It("should cache response and use response's TTL", func() {
|
|
By("first request", func() {
|
|
domain := make(chan bool, 1)
|
|
_ = Bus().SubscribeOnce(CachingResultCacheMiss, func(d string) {
|
|
domain <- true
|
|
})
|
|
|
|
totalCacheCount := make(chan int, 1)
|
|
_ = Bus().SubscribeOnce(CachingResultCacheChanged, func(d int) {
|
|
totalCacheCount <- d
|
|
})
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", A, "123.122.121.120"),
|
|
HaveTTL(BeNumerically("==", 600))))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
|
|
Expect(domain).Should(Receive(Equal(true)))
|
|
Expect(totalCacheCount).Should(Receive(Equal(1)))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(func(g Gomega) {
|
|
domain := make(chan bool, 1)
|
|
_ = Bus().SubscribeOnce(CachingResultCacheHit, func(d string) {
|
|
domain <- true
|
|
})
|
|
|
|
g.Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", A, "123.122.121.120"),
|
|
// ttl is smaller
|
|
HaveTTL(BeNumerically("<=", 599))))
|
|
|
|
// still one call to upstream
|
|
g.Expect(m.Calls).Should(HaveLen(1))
|
|
|
|
g.Expect(domain).Should(Receive(Equal(true)))
|
|
}, "1s").Should(Succeed())
|
|
})
|
|
})
|
|
})
|
|
Context("response TTL is smaller than defined min caching time", func() {
|
|
Context("A query", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 123, A, "123.122.121.120")
|
|
})
|
|
|
|
It("should cache response and use min caching time as TTL", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", A, "123.122.121.120"),
|
|
HaveTTL(BeNumerically("==", 300))))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", A)).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", A, "123.122.121.120"),
|
|
// ttl is smaller
|
|
HaveTTL(BeNumerically("<=", 299))))
|
|
|
|
// still one call to upstream
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("AAAA query", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 123,
|
|
AAAA, "2001:0db8:85a3:08d3:1319:8a2e:0370:7344")
|
|
})
|
|
|
|
It("should cache response and use min caching time as TTL", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", AAAA))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", AAAA, "2001:db8:85a3:8d3:1319:8a2e:370:7344"),
|
|
HaveTTL(BeNumerically("==", 300))))
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", AAAA)).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", AAAA, "2001:db8:85a3:8d3:1319:8a2e:370:7344"),
|
|
// ttl is smaller
|
|
HaveTTL(BeNumerically("<=", 299))))
|
|
|
|
// still one call to upstream
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
When("max caching time is defined", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer(
|
|
"example.com.",
|
|
1230,
|
|
AAAA,
|
|
"2001:0db8:85a3:08d3:1319:8a2e:0370:7344",
|
|
)
|
|
})
|
|
Context("max caching time is negative -> caching is disabled", func() {
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
MaxCachingTime: config.Duration(time.Minute * -1),
|
|
}
|
|
})
|
|
|
|
It("Shouldn't cache any responses", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", AAAA))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", AAAA, "2001:db8:85a3:8d3:1319:8a2e:370:7344"),
|
|
HaveTTL(BeNumerically("==", 1230))))
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", AAAA)).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.", AAAA, "2001:db8:85a3:8d3:1319:8a2e:370:7344"),
|
|
// ttl is smaller
|
|
HaveTTL(BeNumerically("==", 1230))))
|
|
|
|
// one more call to upstream
|
|
Expect(m.Calls).Should(HaveLen(2))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("max caching time is positive", func() {
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
MaxCachingTime: config.Duration(time.Minute * 4),
|
|
}
|
|
})
|
|
It("should cache response and use max caching time as TTL if response TTL is bigger", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", AAAA))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.",
|
|
AAAA, "2001:db8:85a3:8d3:1319:8a2e:370:7344"),
|
|
HaveTTL(BeNumerically("==", 240))))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", AAAA)).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.",
|
|
AAAA, "2001:db8:85a3:8d3:1319:8a2e:370:7344"),
|
|
// ttl is smaller
|
|
HaveTTL(BeNumerically("<=", 239))))
|
|
|
|
// still one call to upstream
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
When("Entry expires in cache", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 1, A, "1.1.1.1")
|
|
})
|
|
Context("max caching time is defined", func() {
|
|
BeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
MaxCachingTime: config.Duration(time.Minute * 1),
|
|
}
|
|
})
|
|
It("should cache response and return 0 TTL if entry is expired", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.",
|
|
A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("==", 1))))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve, "2s").
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", A)).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("example.com.",
|
|
A, "1.1.1.1"),
|
|
// ttl is 0
|
|
HaveTTL(BeNumerically("==", 0))))
|
|
|
|
// still one call to upstream
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Negative cache (caching if upstream resolver returns NXDOMAIN)", func() {
|
|
Context("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("default config should enable negative caching", func() {
|
|
Expect(sutConfig.CacheTimeNegative).Should(BeNumerically(">", 0))
|
|
})
|
|
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", AAAA))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeNameError),
|
|
HaveNoAnswer(),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", AAAA)).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReason("CACHED NEGATIVE"),
|
|
HaveReturnCode(dns.RcodeNameError),
|
|
HaveNoAnswer(),
|
|
))
|
|
|
|
// still one call to resolver
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
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() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", AAAA))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeNameError),
|
|
HaveNoAnswer(),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", AAAA)).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReason(""),
|
|
HaveReturnCode(dns.RcodeNameError),
|
|
HaveNoAnswer(),
|
|
))
|
|
|
|
// one more call to upstream
|
|
Expect(m.Calls).Should(HaveLen(2))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
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)
|
|
})
|
|
|
|
It("response should be cached", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", AAAA))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
HaveNoAnswer(),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("example.com.", AAAA)).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReason("CACHED"),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
HaveNoAnswer(),
|
|
))
|
|
|
|
// still one call to resolver
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
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, MX, "10 alt1.aspmx.l.google.com.")
|
|
})
|
|
It("Should be cached", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("google.de.", MX))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", MX, "alt1.aspmx.l.google.com."),
|
|
HaveTTL(BeNumerically("==", 180)),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("google.de.", MX)).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReason("CACHED"),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", MX, "alt1.aspmx.l.google.com."),
|
|
HaveTTL(BeNumerically("<=", 179)),
|
|
))
|
|
|
|
// still one call to resolver
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Truncated responses should not be cached", func() {
|
|
When("Some query returns truncated response", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, A, "1.1.1.1")
|
|
mockAnswer.Truncated = true
|
|
})
|
|
It("Should not be cached", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("google.de.", A))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("==", 180)),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("google.de.", A))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("==", 180)),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(2))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Responses with CD flag should not be cached", func() {
|
|
When("Some query returns response with CD flag", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, A, "1.1.1.1")
|
|
mockAnswer.CheckingDisabled = true
|
|
})
|
|
It("Should not be cached", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("google.de.", A))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("==", 180)),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("google.de.", A))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("==", 180)),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(2))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("EDNS pseudo records should not be cached", func() {
|
|
When("Some query returns EDNS OPT RRs", func() {
|
|
BeforeEach(func() {
|
|
mockAnswer, _ = util.NewMsgWithAnswer("google.de.", 180, A, "1.1.1.1")
|
|
opt := new(dns.OPT)
|
|
opt.Hdr.Name = "."
|
|
opt.Hdr.Rrtype = dns.TypeOPT
|
|
opt.Option = append(opt.Option, &dns.EDNS0_COOKIE{Code: dns.EDNS0COOKIE, Cookie: "someclientcookie"})
|
|
mockAnswer.Extra = append(mockAnswer.Extra, opt)
|
|
})
|
|
It("Should not be cached", func() {
|
|
By("first request", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("google.de.", A))).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeRESOLVED),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("==", 180)),
|
|
// original response has one ENDS0 Opt
|
|
WithTransform(ToExtra,
|
|
SatisfyAll(
|
|
HaveLen(1),
|
|
)),
|
|
))
|
|
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
|
|
By("second request", func() {
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(newRequest("google.de.", A)).
|
|
Should(SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveReason("CACHED"),
|
|
HaveReturnCode(dns.RcodeSuccess),
|
|
BeDNSRecord("google.de.", A, "1.1.1.1"),
|
|
HaveTTL(BeNumerically("<=", 179)),
|
|
// cached response is without EDNS RRs
|
|
WithTransform(ToExtra,
|
|
SatisfyAll(
|
|
BeEmpty(),
|
|
)),
|
|
))
|
|
|
|
// still one call to resolver
|
|
Expect(m.Calls).Should(HaveLen(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Redis is configured", func() {
|
|
var (
|
|
redisServer *miniredis.Miniredis
|
|
redisClient *redis.Client
|
|
redisConfig *config.Redis
|
|
err error
|
|
)
|
|
BeforeEach(func() {
|
|
redisServer, err = miniredis.Run()
|
|
|
|
Expect(err).Should(Succeed())
|
|
|
|
var rcfg config.Redis
|
|
err = defaults.Set(&rcfg)
|
|
|
|
Expect(err).Should(Succeed())
|
|
|
|
rcfg.Address = redisServer.Addr()
|
|
redisConfig = &rcfg
|
|
redisClient, err = redis.New(context.TODO(), redisConfig)
|
|
|
|
Expect(err).Should(Succeed())
|
|
Expect(redisClient).ShouldNot(BeNil())
|
|
})
|
|
AfterEach(func() {
|
|
redisServer.Close()
|
|
})
|
|
When("cache", func() {
|
|
JustBeforeEach(func() {
|
|
sutConfig = config.CachingConfig{
|
|
MaxCachingTime: config.Duration(time.Second * 10),
|
|
}
|
|
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 1000, A, "1.1.1.1")
|
|
|
|
sut = NewCachingResolver(ctx, sutConfig, redisClient)
|
|
m = &mockResolver{}
|
|
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer}, nil)
|
|
sut.Next(m)
|
|
})
|
|
|
|
It("put in redis", func() {
|
|
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
|
|
Should(HaveResponseType(ResponseTypeRESOLVED))
|
|
|
|
Eventually(func() []string {
|
|
return redisServer.DB(redisConfig.Database).Keys()
|
|
}).Should(HaveLen(1))
|
|
})
|
|
|
|
It("load", func() {
|
|
request := newRequest("example2.com.", A)
|
|
domain := util.ExtractDomain(request.Req.Question[0])
|
|
cacheKey := util.GenerateCacheKey(A, domain)
|
|
redisMockMsg := &redis.CacheMessage{
|
|
Key: cacheKey,
|
|
Response: &Response{
|
|
RType: ResponseTypeCACHED,
|
|
Reason: "MOCK_REDIS",
|
|
Res: mockAnswer,
|
|
},
|
|
}
|
|
redisClient.CacheChannel <- redisMockMsg
|
|
|
|
Eventually(sut.Resolve).
|
|
WithContext(ctx).
|
|
WithArguments(request).
|
|
Should(
|
|
SatisfyAll(
|
|
HaveResponseType(ResponseTypeCACHED),
|
|
HaveTTL(BeNumerically("<=", 10)),
|
|
))
|
|
})
|
|
})
|
|
})
|
|
Context("isRequestCacheable", func() {
|
|
var request *Request
|
|
When("request is not cacheable", func() {
|
|
BeforeEach(func() {
|
|
request = newRequest("example.com.", A)
|
|
e := new(dns.EDNS0_SUBNET)
|
|
e.SourceScope = 0
|
|
e.Address = net.ParseIP("192.168.0.0")
|
|
e.Family = 1
|
|
e.SourceNetmask = 24
|
|
util.SetEdns0Option(request.Req, e)
|
|
})
|
|
|
|
It("should return false", func() {
|
|
Expect(isRequestCacheable(request)).
|
|
Should(BeFalse())
|
|
})
|
|
})
|
|
When("request is cacheable", func() {
|
|
BeforeEach(func() {
|
|
request = newRequest("example.com.", A)
|
|
e := new(dns.EDNS0_SUBNET)
|
|
e.SourceScope = 0
|
|
e.Address = net.ParseIP("192.168.0.10")
|
|
e.Family = 1
|
|
e.SourceNetmask = 32
|
|
util.SetEdns0Option(request.Req, e)
|
|
})
|
|
|
|
It("should return true", func() {
|
|
Expect(isRequestCacheable(request)).
|
|
Should(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
})
|