Enable start as long as at least one upstream resolver in group is reachable (#608)

* Enable start if one upstream resolver fails

* Will now check if upstream actually works

* Fixed default upstream in some tests

* Increase timeouts in some tests

* change default value of "StartVerifyUpstream" to false

Co-authored-by: Dimitri Herzog <dimitri.herzog@gmail.com>
This commit is contained in:
FileGo 2022-08-21 16:21:08 +01:00 committed by GitHub
parent 421807fc22
commit 377f4764fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 37 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea/
.vscode/
*.iml
/*.pem
bin/

View File

@ -399,26 +399,27 @@ func extractNet(upstream string) (NetProtocol, string) {
// Config main configuration
// nolint:maligned
type Config struct {
Upstream UpstreamConfig `yaml:"upstream"`
UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"`
CustomDNS CustomDNSConfig `yaml:"customDNS"`
Conditional ConditionalUpstreamConfig `yaml:"conditional"`
Blocking BlockingConfig `yaml:"blocking"`
ClientLookup ClientLookupConfig `yaml:"clientLookup"`
Caching CachingConfig `yaml:"caching"`
QueryLog QueryLogConfig `yaml:"queryLog"`
Prometheus PrometheusConfig `yaml:"prometheus"`
Redis RedisConfig `yaml:"redis"`
LogLevel log.Level `yaml:"logLevel" default:"info"`
LogFormat log.FormatType `yaml:"logFormat" default:"text"`
LogPrivacy bool `yaml:"logPrivacy" default:"false"`
LogTimestamp bool `yaml:"logTimestamp" default:"true"`
DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"`
HTTPPorts ListenConfig `yaml:"httpPort"`
HTTPSPorts ListenConfig `yaml:"httpsPort"`
TLSPorts ListenConfig `yaml:"tlsPort"`
DoHUserAgent string `yaml:"dohUserAgent"`
MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"`
Upstream UpstreamConfig `yaml:"upstream"`
UpstreamTimeout Duration `yaml:"upstreamTimeout" default:"2s"`
CustomDNS CustomDNSConfig `yaml:"customDNS"`
Conditional ConditionalUpstreamConfig `yaml:"conditional"`
Blocking BlockingConfig `yaml:"blocking"`
ClientLookup ClientLookupConfig `yaml:"clientLookup"`
Caching CachingConfig `yaml:"caching"`
QueryLog QueryLogConfig `yaml:"queryLog"`
Prometheus PrometheusConfig `yaml:"prometheus"`
Redis RedisConfig `yaml:"redis"`
LogLevel log.Level `yaml:"logLevel" default:"info"`
LogFormat log.FormatType `yaml:"logFormat" default:"text"`
LogPrivacy bool `yaml:"logPrivacy" default:"false"`
LogTimestamp bool `yaml:"logTimestamp" default:"true"`
DNSPorts ListenConfig `yaml:"port" default:"[\"53\"]"`
HTTPPorts ListenConfig `yaml:"httpPort"`
HTTPSPorts ListenConfig `yaml:"httpsPort"`
TLSPorts ListenConfig `yaml:"tlsPort"`
DoHUserAgent string `yaml:"dohUserAgent"`
MinTLSServeVer string `yaml:"minTlsServeVersion" default:"1.2"`
StartVerifyUpstream bool `yaml:"startVerifyUpstream" default:"false"`
// Deprecated
DisableIPv6 bool `yaml:"disableIPv6" default:"false"`
CertFile string `yaml:"certFile"`

View File

@ -579,6 +579,7 @@ func defaultTestFileConfig() {
Expect(config.DoHUserAgent).Should(Equal("testBlocky"))
Expect(config.MinTLSServeVer).Should(Equal("1.3"))
Expect(config.StartVerifyUpstream).Should(BeFalse())
Expect(GetConfig()).Should(Not(BeNil()))
}
@ -635,7 +636,8 @@ func writeConfigYml(tmpDir *helpertest.TmpFolder) *helpertest.TmpFile {
"port: 55553,:55554,[::1]:55555",
"logLevel: debug",
"dohUserAgent: testBlocky",
"minTlsServeVersion: 1.3")
"minTlsServeVersion: 1.3",
"startVerifyUpstream: false")
}
func writeConfigDir(tmpDir *helpertest.TmpFolder) error {
@ -695,7 +697,8 @@ func writeConfigDir(tmpDir *helpertest.TmpFolder) error {
"port: 55553,:55554,[::1]:55555",
"logLevel: debug",
"dohUserAgent: testBlocky",
"minTlsServeVersion: 1.3")
"minTlsServeVersion: 1.3",
"startVerifyUpstream: false")
return f2.Error
}

View File

@ -19,6 +19,9 @@ upstream:
# optional: timeout to query the upstream resolver. Default: 2s
upstreamTimeout: 2s
#optional: If true, blocky will fail to start unless at least one upstream server per group is reachable. Default: false
startVerifyUpstream: true
# optional: custom IP address(es) for domain name (with all sub-domains). Multiple addresses must be separated by a comma
# example: query "printer.lan" or "my.printer.lan" will return 192.168.178.3
customDNS:

View File

@ -25,6 +25,7 @@ configuration properties as [JSON](config.yml).
| logPrivacy | bool | no | false | Obfuscate log output (replace all alphanumeric characters with *) for user sensitive data like request domains or responses to increase privacy. |
| dohUserAgent | string | no | | HTTP User Agent for DoH upstreams |
| minTlsServeVersion | string | no | 1.2 | Minimum TLS version that the DoT and DoH server use to serve those encrypted DNS requests |
| startVerifyUpstream | bool | no | false | If true, blocky will fail to start unless at least one upstream server per group is reachable. |
!!! example

View File

@ -197,9 +197,9 @@ var _ = Describe("Downloader", func() {
When("DNS resolution of passed URL fails", func() {
BeforeEach(func() {
sut = NewDownloader(
WithTimeout(100*time.Millisecond),
WithTimeout(500*time.Millisecond),
WithAttempts(3),
WithCooldown(time.Millisecond))
WithCooldown(200*time.Millisecond))
})
It("Should perform a retry until max retry attempt count is reached and return DNSError", func() {
reader, err := sut.DownloadFile("http://some.domain.which.does.not.exist")

View File

@ -26,7 +26,8 @@ var (
// Bootstrap allows resolving hostnames using the configured bootstrap DNS.
type Bootstrap struct {
log *logrus.Entry
log *logrus.Entry
startVerifyUpstream bool
resolver Resolver
upstream Resolver // the upstream that's part of the above resolver
@ -64,9 +65,10 @@ func NewBootstrap(cfg *config.Config) (b *Bootstrap, err error) {
// This also prevents the GC to clean up these two structs, but is not currently an
// issue since they stay allocated until the process terminates
b = &Bootstrap{
log: log,
upstreamIPs: ips,
systemResolver: net.DefaultResolver, // allow replacing it during tests
log: log,
upstreamIPs: ips,
systemResolver: net.DefaultResolver, // allow replacing it during tests
startVerifyUpstream: cfg.StartVerifyUpstream,
}
if upstream.IsDefault() {

View File

@ -10,6 +10,7 @@ import (
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
"github.com/mroth/weightedrand"
"github.com/sirupsen/logrus"
@ -36,23 +37,56 @@ type requestResponse struct {
err error
}
// testResolver sends a test query to verify the resolver is reachable and working
func testResolver(r *UpstreamResolver) error {
request := newRequest("github.com.", dns.Type(dns.TypeA))
resp, err := r.Resolve(request)
if err != nil || resp.RType != model.ResponseTypeRESOLVED {
return fmt.Errorf("test resolve of upstream server failed: %w", err)
}
return nil
}
// NewParallelBestResolver creates new resolver instance
func NewParallelBestResolver(upstreamResolvers map[string][]config.Upstream, bootstrap *Bootstrap) (Resolver, error) {
s := make(map[string][]*upstreamResolverStatus, len(upstreamResolvers))
logger := logger("parallel resolver")
s := make(map[string][]*upstreamResolverStatus)
for name, res := range upstreamResolvers {
resolvers := make([]*upstreamResolverStatus, len(res))
var resolvers []*upstreamResolverStatus
for i, u := range res {
var errResolvers int
for _, u := range res {
r, err := NewUpstreamResolver(u, bootstrap)
if err != nil {
return nil, err
logger.Warnf("upstream group %s: %v", name, err)
errResolvers++
continue
}
resolvers[i] = &upstreamResolverStatus{
if bootstrap != skipUpstreamCheck {
err = testResolver(r)
if err != nil {
logger.Warn(err)
errResolvers++
}
}
resolver := &upstreamResolverStatus{
resolver: r,
}
resolvers[i].lastErrorTime.Store(time.Unix(0, 0))
resolver.lastErrorTime.Store(time.Unix(0, 0))
resolvers = append(resolvers, resolver)
}
if bootstrap != skipUpstreamCheck {
if bootstrap.startVerifyUpstream && errResolvers == len(res) {
return nil, fmt.Errorf("unable to reach any DNS resolvers configured for resolver group %s", name)
}
}
s[name] = resolvers

View File

@ -26,6 +26,67 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() {
})
})
Describe("Some default upstream resolvers cannot be reached", func() {
It("should start normally", func() {
skipUpstreamCheck.startVerifyUpstream = true
mockUpstream := NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) {
response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 123, dns.Type(dns.TypeA), "123.124.122.122")
return
})
defer mockUpstream.Close()
upstream := map[string][]config.Upstream{
upstreamDefaultCfgName: {
config.Upstream{
Host: "wrong",
},
mockUpstream.Start(),
},
}
_, err := NewParallelBestResolver(upstream, skipUpstreamCheck)
Expect(err).Should(Not(HaveOccurred()))
})
})
Describe("All default upstream resolvers cannot be reached", func() {
var (
upstream map[string][]config.Upstream
b *Bootstrap
)
BeforeEach(func() {
b = TestBootstrap(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure}})
upstream = map[string][]config.Upstream{
upstreamDefaultCfgName: {
config.Upstream{
Host: "wrong",
},
config.Upstream{
Host: "127.0.0.2",
},
},
}
})
It("should fail to start if strict checking is enabled", func() {
b.startVerifyUpstream = true
_, err := NewParallelBestResolver(upstream, b)
Expect(err).Should(HaveOccurred())
})
It("should start if strict checking is disabled", func() {
b.startVerifyUpstream = false
_, err := NewParallelBestResolver(upstream, b)
Expect(err).Should(Not(HaveOccurred()))
})
})
Describe("Resolving result from fastest upstream resolver", func() {
var (
sut Resolver
@ -310,6 +371,8 @@ var _ = Describe("ParallelBestResolver", Label("parallelBestResolver"), func() {
sut Resolver
)
BeforeEach(func() {
config.GetConfig().StartVerifyUpstream = false
sut, _ = NewParallelBestResolver(map[string][]config.Upstream{upstreamDefaultCfgName: {
{Host: "host1"},
{Host: "host2"},

View File

@ -61,8 +61,15 @@ var _ = BeforeSuite(func() {
DeferCleanup(fritzboxMockUpstream.Close)
clientMockUpstream := resolver.NewMockUDPUpstreamServer().WithAnswerFn(func(request *dns.Msg) (response *dns.Msg) {
var clientName string
client := mockClientName.Load()
if client != nil {
clientName = mockClientName.Load().(string)
}
response, err := util.NewMsgWithAnswer(
util.ExtractDomain(request.Question[0]), 3600, dns.Type(dns.TypePTR), mockClientName.Load().(string),
util.ExtractDomain(request.Question[0]), 3600, dns.Type(dns.TypePTR), clientName,
)
Expect(err).Should(Succeed())
@ -550,7 +557,7 @@ var _ = Describe("Running DNS server", func() {
Expect(cErr).Should(Succeed())
cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "4.4.4.4", Port: 53}}}
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "1.1.1.1", Port: 53}}}
cfg.Redis.Address = "test-fail"
})
@ -680,7 +687,7 @@ var _ = Describe("Running DNS server", func() {
Expect(cErr).Should(Succeed())
cfg.Upstream.ExternalResolvers = map[string][]config.Upstream{
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "4.4.4.4", Port: 53}}}
"default": {config.Upstream{Net: config.NetProtocolTcpUdp, Host: "1.1.1.1", Port: 53}}}
})
It("should create self-signed certificate if key/cert files are not provided", func() {