blocky/resolver/hosts_file_resolver_test.go

376 lines
11 KiB
Go
Raw Normal View History

package resolver
import (
"context"
"time"
"github.com/0xERR0R/blocky/config"
. "github.com/0xERR0R/blocky/helpertest"
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
"github.com/0xERR0R/blocky/log"
. "github.com/0xERR0R/blocky/model"
"github.com/miekg/dns"
2022-03-03 11:27:27 +01:00
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
)
var _ = Describe("HostsFileResolver", func() {
var (
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
TTL = uint32(time.Now().Second())
sut *HostsFileResolver
sutConfig config.HostsFile
m *mockResolver
tmpDir *TmpFolder
tmpFile *TmpFile
err error
resp *Response
ctx context.Context
cancelFn context.CancelFunc
)
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
Describe("Type", func() {
It("follows conventions", func() {
expectValidResolverType(sut)
})
})
BeforeEach(func() {
ctx, cancelFn = context.WithCancel(context.Background())
DeferCleanup(cancelFn)
tmpDir = NewTmpFolder("HostsFileResolver")
tmpFile = writeHostFile(tmpDir)
sutConfig = config.HostsFile{
2023-04-17 18:21:56 +02:00
Sources: config.NewBytesSources(tmpFile.Path),
HostsTTL: config.Duration(time.Duration(TTL) * time.Second),
FilterLoopback: true,
Loading: config.SourceLoading{
2023-04-17 18:21:56 +02:00
RefreshPeriod: -1,
MaxErrorsPerSource: 5,
},
}
})
JustBeforeEach(func() {
sut, err = NewHostsFileResolver(ctx, sutConfig, systemResolverBootstrap)
2023-04-17 18:21:56 +02:00
Expect(err).Should(Succeed())
m = &mockResolver{}
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)
sut.Next(m)
})
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
Describe("IsEnabled", func() {
It("is true", func() {
Expect(sut.IsEnabled()).Should(BeTrue())
})
})
Describe("LogConfig", func() {
It("should log something", func() {
logger, hook := log.NewMockEntry()
sut.LogConfig(logger)
Expect(hook.Calls).ShouldNot(BeEmpty())
})
})
Describe("Using hosts file", func() {
When("Hosts file cannot be located", func() {
BeforeEach(func() {
sutConfig = config.HostsFile{
2023-04-17 18:21:56 +02:00
Sources: config.NewBytesSources("/this/file/does/not/exist"),
HostsTTL: config.Duration(time.Duration(TTL) * time.Second),
}
})
It("should not parse any hosts", func() {
2023-04-17 18:21:56 +02:00
Expect(sut.cfg.Sources).ShouldNot(BeEmpty())
Expect(sut.hosts.v4.hosts).Should(BeEmpty())
Expect(sut.hosts.v6.hosts).Should(BeEmpty())
Expect(sut.hosts.v4.aliases).Should(BeEmpty())
Expect(sut.hosts.v6.aliases).Should(BeEmpty())
Expect(sut.hosts.isEmpty()).Should(BeTrue())
})
It("should go to next resolver on query", func() {
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeRESOLVED),
HaveReturnCode(dns.RcodeSuccess),
))
m.AssertExpectations(GinkgoT())
})
})
When("Hosts file is not set", func() {
BeforeEach(func() {
2023-04-17 18:21:56 +02:00
sutConfig.Deprecated.Filepath = new(config.BytesSource)
sutConfig.Sources = make([]config.BytesSource, 0)
})
JustBeforeEach(func() {
err = sut.loadSources(ctx)
Expect(err).Should(Succeed())
})
It("should go to next resolver on query", func() {
Expect(sut.Resolve(ctx, newRequest("example.com.", A))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeRESOLVED),
HaveReturnCode(dns.RcodeSuccess),
))
m.AssertExpectations(GinkgoT())
})
})
When("Hosts file can be located", func() {
It("should parse it successfully", func() {
Expect(sut).ShouldNot(BeNil())
Expect(sut.hosts.v4.hosts).Should(HaveLen(5))
Expect(sut.hosts.v6.hosts).Should(HaveLen(2))
Expect(sut.hosts.v4.aliases).Should(HaveLen(4))
Expect(sut.hosts.v6.aliases).Should(HaveLen(2))
})
When("filterLoopback is false", func() {
BeforeEach(func() {
sutConfig.FilterLoopback = false
})
It("should parse it successfully", func() {
Expect(sut).ShouldNot(BeNil())
Expect(sut.hosts.v4.hosts).Should(HaveLen(7))
Expect(sut.hosts.v6.hosts).Should(HaveLen(3))
Expect(sut.hosts.v4.aliases).Should(HaveLen(5))
Expect(sut.hosts.v6.aliases).Should(HaveLen(2))
})
})
})
When("Hosts file has too many errors", func() {
BeforeEach(func() {
tmpFile = tmpDir.CreateStringFile("hosts-too-many-errors.txt",
"invalidip localhost",
"127.0.0.1 localhost", // ok
"127.0.0.1 # no host",
"127.0.0.1 invalidhost!",
"a.b.c.d localhost",
"127.0.0.x localhost",
"256.0.0.1 localhost",
)
2023-04-17 18:21:56 +02:00
sutConfig.Sources = config.NewBytesSources(tmpFile.Path)
})
It("should not be used", func() {
Expect(sut).ShouldNot(BeNil())
2023-04-17 18:21:56 +02:00
Expect(sut.cfg.Sources).ShouldNot(BeEmpty())
refactor: configuration rework (usage and printing) (#920) * refactor: make `config.Duration` a struct with `time.Duration` embed Allows directly calling `time.Duration` methods. * refactor(HostsFileResolver): don't copy individual config items The idea is to make adding configuration options easier, and searching for references straight forward. * refactor: move config printing to struct and use a logger Using a logger allows using multiple levels so the whole configuration can be printed in trace/verbose mode, but only important parts are shown by default. * squash: rename `Cast` to `ToDuration` * squash: revert `Duration` to a simple wrapper ("new type" pattern) * squash: `Duration.IsZero` tests * squash: refactor resolvers to rely on their config directly if possible * squash: implement `IsEnabled` and `LogValues` for all resolvers * refactor: use go-enum `--values` to simplify getting all log fields * refactor: simplify `QType` unmarshaling * squash: rename `ValueLogger` to `Configurable` * squash: rename `UpstreamConfig` to `ParallelBestConfig` * squash: rename `RewriteConfig` to `RewriterConfig` * squash: config tests * squash: resolver tests * squash: add `ForEach` test and improve `Chain` ones * squash: simplify implementing `config.Configurable` * squash: minor changes for better coverage * squash: more `UnmarshalYAML` -> `UnmarshalText` * refactor: move `config.Upstream` into own file * refactor: add `Resolver.Type` method * squash: add `log` method to `typed` to use `Resolover.Type` as prefix * squash: tweak startup config logging * squash: add `LogResolverConfig` tests * squash: make sure all options of type `Duration` use `%s`
2023-03-12 22:14:10 +01:00
Expect(sut.hosts.v4.hosts).Should(BeEmpty())
Expect(sut.hosts.v6.hosts).Should(BeEmpty())
Expect(sut.hosts.v4.aliases).Should(BeEmpty())
Expect(sut.hosts.v6.aliases).Should(BeEmpty())
})
})
When("IPv4 mapping is defined for a host", func() {
It("defined ipv4 query should be resolved", func() {
Expect(sut.Resolve(ctx, newRequest("ipv4host.", A))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
BeDNSRecord("ipv4host.", A, "192.168.2.1"),
HaveTTL(BeNumerically("==", TTL)),
))
})
It("defined ipv4 query for alias should be resolved", func() {
Expect(sut.Resolve(ctx, newRequest("router2.", A))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
BeDNSRecord("router2.", A, "10.0.0.1"),
HaveTTL(BeNumerically("==", TTL)),
))
})
It("ipv4 query should return NOERROR and empty result", func() {
Expect(sut.Resolve(ctx, newRequest("does.not.exist.", A))).
Should(
SatisfyAll(
HaveNoAnswer(),
HaveReturnCode(dns.RcodeSuccess),
HaveResponseType(ResponseTypeRESOLVED),
))
})
})
When("IPv6 mapping is defined for a host", func() {
It("defined ipv6 query should be resolved", func() {
Expect(sut.Resolve(ctx, newRequest("ipv6host.", AAAA))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
BeDNSRecord("ipv6host.", AAAA, "faaf:faaf:faaf:faaf::1"),
HaveTTL(BeNumerically("==", TTL)),
))
})
It("ipv6 query should return NOERROR and empty result", func() {
Expect(sut.Resolve(ctx, newRequest("does.not.exist.", AAAA))).
Should(
SatisfyAll(
HaveNoAnswer(),
HaveReturnCode(dns.RcodeSuccess),
HaveResponseType(ResponseTypeRESOLVED),
))
})
})
When("the domain is not known", func() {
It("calls the next resolver", func() {
resp, err = sut.Resolve(ctx, newRequest("not-in-hostsfile.tld.", A))
Expect(err).Should(Succeed())
Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
m.AssertExpectations(GinkgoT())
})
})
When("the question type is not handled", func() {
It("calls the next resolver", func() {
resp, err = sut.Resolve(ctx, newRequest("localhost.", MX))
Expect(err).Should(Succeed())
Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
m.AssertExpectations(GinkgoT())
})
})
When("Reverse DNS request is received", func() {
It("should resolve the defined domain name", func() {
By("ipv4 with one hostname", func() {
Expect(sut.Resolve(ctx, newRequest("2.0.0.10.in-addr.arpa.", PTR))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
BeDNSRecord("2.0.0.10.in-addr.arpa.", PTR, "router3."),
HaveTTL(BeNumerically("==", TTL)),
))
})
By("ipv4 with aliases", func() {
Expect(sut.Resolve(ctx, newRequest("1.0.0.10.in-addr.arpa.", PTR))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
WithTransform(ToAnswer, ContainElements(
BeDNSRecord("1.0.0.10.in-addr.arpa.", PTR, "router0."),
BeDNSRecord("1.0.0.10.in-addr.arpa.", PTR, "router1."),
BeDNSRecord("1.0.0.10.in-addr.arpa.", PTR, "router2."),
)),
))
})
By("ipv6", func() {
Expect(sut.Resolve(ctx,
newRequest("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.a.a.f.f.a.a.f.f.a.a.f.f.a.a.f.ip6.arpa.", PTR)),
).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
WithTransform(ToAnswer, ContainElements(
BeDNSRecord("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.a.a.f.f.a.a.f.f.a.a.f.f.a.a.f.ip6.arpa.",
PTR, "ipv6host."),
BeDNSRecord("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.a.a.f.f.a.a.f.f.a.a.f.f.a.a.f.ip6.arpa.",
PTR, "ipv6host.local.lan."),
)),
))
})
})
It("should ignore invalid PTR", func() {
resp, err = sut.Resolve(ctx, newRequest("2.0.0.10.in-addr.fail.arpa.", PTR))
Expect(err).Should(Succeed())
Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
m.AssertExpectations(GinkgoT())
})
When("filterLoopback is true", func() {
It("calls the next resolver", func() {
resp, err = sut.Resolve(ctx, newRequest("1.0.0.127.in-addr.arpa.", PTR))
Expect(err).Should(Succeed())
Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
m.AssertExpectations(GinkgoT())
})
})
When("the IP is not known", func() {
It("calls the next resolver", func() {
resp, err = sut.Resolve(ctx, newRequest("255.255.255.255.in-addr.arpa.", PTR))
Expect(err).Should(Succeed())
Expect(resp).ShouldNot(HaveResponseType(ResponseTypeHOSTSFILE))
m.AssertExpectations(GinkgoT())
})
})
When("filterLoopback is false", func() {
BeforeEach(func() {
sutConfig.FilterLoopback = false
})
It("resolve the defined domain name", func() {
Expect(sut.Resolve(ctx, newRequest("1.1.0.127.in-addr.arpa.", PTR))).
Should(
SatisfyAll(
HaveResponseType(ResponseTypeHOSTSFILE),
HaveReturnCode(dns.RcodeSuccess),
WithTransform(ToAnswer, ContainElements(
BeDNSRecord("1.1.0.127.in-addr.arpa.", PTR, "localhost2."),
BeDNSRecord("1.1.0.127.in-addr.arpa.", PTR, "localhost2.local.lan."),
)),
))
})
})
})
})
Describe("Delegating to next resolver", func() {
When("no hosts file is provided", func() {
It("should delegate to next resolver", func() {
_, err = sut.Resolve(ctx, newRequest("example.com.", A))
Expect(err).Should(Succeed())
// delegate was executed
m.AssertExpectations(GinkgoT())
})
})
})
})
func writeHostFile(tmpDir *TmpFolder) *TmpFile {
return tmpDir.CreateStringFile("hosts.txt",
"# Random comment",
"127.0.0.1 localhost",
"127.0.1.1 localhost2 localhost2.local.lan",
"::1 localhost",
"# Two empty lines to follow",
"",
"",
"faaf:faaf:faaf:faaf::1 ipv6host ipv6host.local.lan",
"192.168.2.1 ipv4host ipv4host.local.lan",
"faaf:faaf:faaf:faaf::2 dualhost dualhost.local.lan",
"192.168.2.2 dualhost dualhost.local.lan",
"10.0.0.1 router0 router1 router2",
"10.0.0.2 router3 # Another comment",
"10.0.0.3 router4#comment without a space",
"10.0.0.4 # Invalid entry",
"300.300.300.300 invalid4 # Invalid IPv4",
"abcd:efgh:ijkl::1 invalid6 # Invalid IPv6",
"1.2.3.4 localhost", // localhost name but not localhost IP
// from https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
"fe80::1%lo0 localhost", // interface name
)
}