#79: Support for multiple conditional forwarders per domain

This commit is contained in:
Dimitri Herzog 2020-12-27 23:40:27 +01:00
parent 4d69c26ae2
commit 914a04e5b1
11 changed files with 104 additions and 64 deletions

View File

@ -57,6 +57,34 @@ func (u *Upstream) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}
func (c *ConditionalUpstreamMapping) UnmarshalYAML(unmarshal func(interface{}) error) error {
var input map[string]string
if err := unmarshal(&input); err != nil {
return err
}
result := make(map[string][]Upstream)
for k, v := range input {
var upstreams []Upstream
for _, part := range strings.Split(v, ",") {
upstream, err := ParseUpstream(strings.TrimSpace(part))
if err != nil {
return err
}
upstreams = append(upstreams, upstream)
}
result[k] = upstreams
}
c.Upstreams = result
return nil
}
// ParseUpstream creates new Upstream from passed string in format [net]:host[:port][/path]
func ParseUpstream(upstream string) (result Upstream, err error) {
if strings.TrimSpace(upstream) == "" {
@ -190,7 +218,11 @@ type CustomDNSConfig struct {
}
type ConditionalUpstreamConfig struct {
Mapping map[string]Upstream `yaml:"mapping"`
Mapping ConditionalUpstreamMapping `yaml:"mapping"`
}
type ConditionalUpstreamMapping struct {
Upstreams map[string][]Upstream
}
type BlockingConfig struct {

View File

@ -28,7 +28,9 @@ var _ = Describe("Config", func() {
Expect(cfg.Upstream.ExternalResolvers[2].Host).Should(Equal("1.1.1.1"))
Expect(cfg.CustomDNS.Mapping).Should(HaveLen(1))
Expect(cfg.CustomDNS.Mapping["my.duckdns.org"]).Should(Equal(net.ParseIP("192.168.178.3")))
Expect(cfg.Conditional.Mapping).Should(HaveLen(1))
Expect(cfg.Conditional.Mapping.Upstreams).Should(HaveLen(2))
Expect(cfg.Conditional.Mapping.Upstreams["fritz.box"]).Should(HaveLen(1))
Expect(cfg.Conditional.Mapping.Upstreams["multiple.resolvers"]).Should(HaveLen(2))
Expect(cfg.ClientLookup.Upstream.Host).Should(Equal("192.168.178.1"))
Expect(cfg.ClientLookup.SingleNameOrder).Should(Equal([]uint{2, 1}))
Expect(cfg.Blocking.BlackLists).Should(HaveLen(2))

View File

@ -35,34 +35,35 @@ Create `config.yml` file with your configuration [as yml](config.yml):
```yml
upstream:
# these external DNS resolvers will be used. Blocky picks 2 random resolvers from the list for each query
# format for resolver: [net:]host:[port][/path]. net could be empty (default, shortcut for tcp+udp), tcp+udp, tcp, udp, tcp-tls or https (DoH). If port is empty, default port will be used (53 for udp and tcp, 853 for tcp-tls, 443 for https (Doh))
externalResolvers:
- 46.182.19.48
- 80.241.218.68
- tcp-tls:fdns1.dismail.de:853
- https://dns.digitale-gesellschaft.ch/dns-query
# format for resolver: [net:]host:[port][/path]. net could be empty (default, shortcut for tcp+udp), tcp+udp, tcp, udp, tcp-tls or https (DoH). If port is empty, default port will be used (53 for udp and tcp, 853 for tcp-tls, 443 for https (Doh))
externalResolvers:
- 46.182.19.48
- 80.241.218.68
- tcp-tls:fdns1.dismail.de:853
- https://dns.digitale-gesellschaft.ch/dns-query
# optional: custom IP address for domain name (with all sub-domains)
# example: query "printer.lan" or "my.printer.lan" will return 192.168.178.3
customDNS:
mapping:
printer.lan: 192.168.178.3
mapping:
printer.lan: 192.168.178.3
# optional: definition, which DNS resolver should be used for queries to the domain (with all sub-domains).
# optional: definition, which DNS resolver(s) should be used for queries to the domain (with all sub-domains). Multiple resolvers must be separated by comma
# Example: Query client.fritz.box will ask DNS server 192.168.178.1. This is necessary for local network, to resolve clients by host name
conditional:
mapping:
fritz.box: udp:192.168.178.1
mapping:
fritz.box: udp:192.168.178.1
lan.net: udp:192.168.178.1,udp:192.168.178.2
# optional: use black and white lists to block queries (for example ads, trackers, adult pages etc.)
blocking:
# definition of blacklist groups. Can be external link (http/https) or local file
blackLists:
ads:
- https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://mirror1.malwaredomains.com/files/justdomains
- http://sysctl.org/cameleon/hosts
# definition of blacklist groups. Can be external link (http/https) or local file
blackLists:
ads:
- https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://mirror1.malwaredomains.com/files/justdomains
- http://sysctl.org/cameleon/hosts
- https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist
- https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
special:

View File

@ -13,11 +13,12 @@ customDNS:
mapping:
printer.lan: 192.168.178.3
# optional: definition, which DNS resolver should be used for queries to the domain (with all sub-domains).
# optional: definition, which DNS resolver(s) should be used for queries to the domain (with all sub-domains). Multiple resolvers must be separated by comma
# Example: Query client.fritz.box will ask DNS server 192.168.178.1. This is necessary for local network, to resolve clients by host name
conditional:
mapping:
fritz.box: udp:192.168.178.1
lan.net: udp:192.168.178.1,udp:192.168.178.2
# optional: use black and white lists to block queries (for example ads, trackers, adult pages etc.)
blocking:

View File

@ -17,8 +17,8 @@ type ConditionalUpstreamResolver struct {
func NewConditionalUpstreamResolver(cfg config.ConditionalUpstreamConfig) ChainedResolver {
m := make(map[string]Resolver)
for domain, upstream := range cfg.Mapping {
m[strings.ToLower(domain)] = NewUpstreamResolver(upstream)
for domain, upstream := range cfg.Mapping.Upstreams {
m[strings.ToLower(domain)] = NewParallelBestResolver(upstream)
}
return &ConditionalUpstreamResolver{mapping: m}

View File

@ -25,18 +25,19 @@ var _ = Describe("ConditionalUpstreamResolver", func() {
BeforeEach(func() {
sut = NewConditionalUpstreamResolver(config.ConditionalUpstreamConfig{
Mapping: map[string]config.Upstream{
"fritz.box": TestUDPUpstream(func(request *dns.Msg) (response *dns.Msg) {
response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 123, dns.TypeA, "123.124.122.122")
Mapping: config.ConditionalUpstreamMapping{
Upstreams: map[string][]config.Upstream{
"fritz.box": {TestUDPUpstream(func(request *dns.Msg) (response *dns.Msg) {
response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 123, dns.TypeA, "123.124.122.122")
return response
}),
"other.box": TestUDPUpstream(func(request *dns.Msg) (response *dns.Msg) {
response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 250, dns.TypeA, "192.192.192.192")
return response
})},
"other.box": {TestUDPUpstream(func(request *dns.Msg) (response *dns.Msg) {
response, _ = util.NewMsgWithAnswer(request.Question[0].Name, 250, dns.TypeA, "192.192.192.192")
return response
}),
},
return response
})},
}},
})
m = &resolverMock{}
m.On("Resolve", mock.Anything).Return(&Response{Res: new(dns.Msg)}, nil)

View File

@ -5,6 +5,7 @@ import (
"blocky/util"
"fmt"
"math"
"strings"
"time"
"github.com/mroth/weightedrand"
@ -26,10 +27,10 @@ type requestResponse struct {
err error
}
func NewParallelBestResolver(cfg config.UpstreamConfig) Resolver {
resolvers := make([]*upstreamResolverStatus, len(cfg.ExternalResolvers))
func NewParallelBestResolver(upstreamResolvers []config.Upstream) Resolver {
resolvers := make([]*upstreamResolverStatus, len(upstreamResolvers))
for i, u := range cfg.ExternalResolvers {
for i, u := range upstreamResolvers {
resolvers[i] = &upstreamResolverStatus{
resolver: NewUpstreamResolver(u),
lastErrorTime: time.Unix(0, 0),
@ -48,11 +49,20 @@ func (r *ParallelBestResolver) Configuration() (result []string) {
return
}
func (r ParallelBestResolver) String() string {
result := make([]string, len(r.resolvers))
for i, s := range r.resolvers {
result[i] = fmt.Sprintf("%s", s.resolver)
}
return fmt.Sprintf("parallel upstreams '%s'", strings.Join(result, "; "))
}
func (r *ParallelBestResolver) Resolve(request *Request) (*Response, error) {
logger := request.Log.WithField("prefix", "parallel_best_resolver")
if len(r.resolvers) == 1 {
logger.WithField("resolver", r.resolvers[0]).Debug("delegating to resolver")
logger.WithField("resolver", r.resolvers[0].resolver).Debug("delegating to resolver")
return r.resolvers[0].resolver.Resolve(request)
}
@ -63,11 +73,11 @@ func (r *ParallelBestResolver) Resolve(request *Request) (*Response, error) {
var collectedErrors []error
logger.WithField("resolver", r1).Debug("delegating to resolver")
logger.WithField("resolver", r1.resolver).Debug("delegating to resolver")
go resolve(request, r1, ch)
logger.WithField("resolver", r2).Debug("delegating to resolver")
logger.WithField("resolver", r2.resolver).Debug("delegating to resolver")
go resolve(request, r2, ch)
@ -80,7 +90,7 @@ func (r *ParallelBestResolver) Resolve(request *Request) (*Response, error) {
collectedErrors = append(collectedErrors, result.err)
} else {
logger.WithFields(logrus.Fields{
"resolver": r1,
"resolver": r1.resolver,
"answer": util.AnswerToString(result.response.Res.Answer),
}).Debug("using response from resolver")
return result.response, nil

View File

@ -37,9 +37,7 @@ var _ = Describe("ParallelBestResolver", func() {
Expect(err).Should(Succeed())
return response
})
sut = NewParallelBestResolver(config.UpstreamConfig{
ExternalResolvers: []config.Upstream{fast, slow},
})
sut = NewParallelBestResolver([]config.Upstream{fast, slow})
})
It("Should use result from fastest one", func() {
request := newRequest("example.com.", dns.TypeA)
@ -63,9 +61,7 @@ var _ = Describe("ParallelBestResolver", func() {
Expect(err).Should(Succeed())
return response
})
sut = NewParallelBestResolver(config.UpstreamConfig{
ExternalResolvers: []config.Upstream{withError, slow},
})
sut = NewParallelBestResolver([]config.Upstream{withError, slow})
})
It("Should use result from successful resolver", func() {
request := newRequest("example.com.", dns.TypeA)
@ -83,9 +79,7 @@ var _ = Describe("ParallelBestResolver", func() {
withError1 := config.Upstream{Host: "wrong"}
withError2 := config.Upstream{Host: "wrong"}
sut = NewParallelBestResolver(config.UpstreamConfig{
ExternalResolvers: []config.Upstream{withError1, withError2},
})
sut = NewParallelBestResolver([]config.Upstream{withError1, withError2})
})
It("Should return error", func() {
request := newRequest("example.com.", dns.TypeA)
@ -104,9 +98,7 @@ var _ = Describe("ParallelBestResolver", func() {
Expect(err).Should(Succeed())
return response
})
sut = NewParallelBestResolver(config.UpstreamConfig{
ExternalResolvers: []config.Upstream{fast},
})
sut = NewParallelBestResolver([]config.Upstream{fast})
})
It("Should use result from defined resolver", func() {
request := newRequest("example.com.", dns.TypeA)
@ -137,9 +129,7 @@ var _ = Describe("ParallelBestResolver", func() {
return response
})
sut := NewParallelBestResolver(config.UpstreamConfig{
ExternalResolvers: []config.Upstream{withError1, fast1, fast2, withError2},
}).(*ParallelBestResolver)
sut := NewParallelBestResolver([]config.Upstream{withError1, fast1, fast2, withError2}).(*ParallelBestResolver)
By("all resolvers have same weight for random -> equal distribution", func() {
resolverCount := make(map[Resolver]int)
@ -194,11 +184,9 @@ var _ = Describe("ParallelBestResolver", func() {
Describe("Configuration output", func() {
BeforeEach(func() {
sut = NewParallelBestResolver(config.UpstreamConfig{
ExternalResolvers: []config.Upstream{
{Host: "host1"},
{Host: "host2"},
},
sut = NewParallelBestResolver([]config.Upstream{
{Host: "host1"},
{Host: "host2"},
})
})
It("should return configuration", func() {

View File

@ -115,7 +115,7 @@ func createQueryResolver(cfg *config.Config, router *chi.Mux) resolver.Resolver
resolver.NewCustomDNSResolver(cfg.CustomDNS),
resolver.NewBlockingResolver(router, cfg.Blocking),
resolver.NewCachingResolver(cfg.Caching),
resolver.NewParallelBestResolver(cfg.Upstream),
resolver.NewParallelBestResolver(cfg.Upstream.ExternalResolvers),
)
}

View File

@ -65,7 +65,11 @@ var _ = Describe("Running DNS server", func() {
},
},
Conditional: config.ConditionalUpstreamConfig{
Mapping: map[string]config.Upstream{"fritz.box": upstreamFritzbox},
Mapping: config.ConditionalUpstreamMapping{
Upstreams: map[string][]config.Upstream{
"fritz.box": {upstreamFritzbox},
},
},
},
Blocking: config.BlockingConfig{
BlackLists: map[string][]string{

1
testdata/config.yml vendored
View File

@ -9,6 +9,7 @@ customDNS:
conditional:
mapping:
fritz.box: udp:192.168.178.1
multiple.resolvers: udp:192.168.178.1,udp:192.168.178.2
blocking:
blackLists:
ads: