Skip to content
Commits on Source (1)
  • Dimitry Kolyshev's avatar
    Pull request: proxy: upstream config · b8be799e
    Dimitry Kolyshev authored
    Merge in DNS/dnsproxy from 4503-upstream-conf to master
    
    Squashed commit of the following:
    
    commit abf53fe685fe04a851a1a74d0811d4f34466d1dc
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Fri May 13 15:51:58 2022 +0200
    
        proxy: imp code
    
    commit e00ac4e33219132424d50b88f9f90f77607c5e1a
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Fri May 13 15:48:33 2022 +0200
    
        proxy: bench
    
    commit 899aa7f5551b167f4f831809505e178603a68dd1
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Fri May 13 10:32:28 2022 +0200
    
        proxy: imp code
    
    commit 156efa8c41502e9d4c31ea94877c4349c13f0735
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Wed May 11 14:10:36 2022 +0200
    
        proxy: imp code
    
    commit 92fa2820487bd273e9a457ae62f83c25ed7c71c2
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Wed May 11 14:08:01 2022 +0200
    
        proxy: upper level check
    
    commit 841d05246f41f7efa5ca3717c1901c0f848633da
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Wed May 11 11:53:26 2022 +0200
    
        proxy: imp code
    
    commit 6725791a
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Fri May 6 16:51:34 2022 +0200
    
        proxy: wildcard exclusion
    
    commit 35d69b4f
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Fri May 6 15:12:48 2022 +0200
    
        proxy: wildcard exclusion
    
    commit 53cb4295
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Fri Apr 29 12:59:45 2022 +0200
    
        proxy: imp code
    
    commit f1328a96
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Thu Apr 28 20:16:34 2022 +0200
    
        proxy: imp code
    
    commit 79c0ef63
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Thu Apr 28 17:14:22 2022 +0200
    
        proxy: imp code
    
    commit 70622f4d
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Thu Apr 28 17:09:01 2022 +0200
    
        all: docs
    
    commit 1332e459
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Thu Apr 28 16:52:03 2022 +0200
    
        proxy: upstream config
    b8be799e
......@@ -252,7 +252,8 @@ If one or more domains are specified, that upstream (`upstreamString`) is used o
1. An empty domain specification, // has the special meaning of "unqualified names only" ie names without any dots in them.
2. More specific domains take precedence over less specific domains, so: `--upstream=[/host.com/]1.2.3.4 --upstream=[/www.host.com/]2.3.4.5` will send queries for *.host.com to 1.2.3.4, except *.www.host.com, which will go to 2.3.4.5
3. The special server address '#' means, "use the standard servers", so: `--upstream=[/host.com/]1.2.3.4 --upstream=[/www.host.com/]#` will send queries for *.host.com to 1.2.3.4, except *.www.host.com which will be forwarded as usual.
3. The special server address `#` means, "use the standard servers", so: `--upstream=[/host.com/]1.2.3.4 --upstream=[/www.host.com/]#` will send queries for \*.host.com to 1.2.3.4, except \*.www.host.com which will be forwarded as usual.
4. The wildcard `*` has special meaning of "any sub-domain", so: `--upstream=[/*.host.com/]1.2.3.4` will send queries for \*.host.com to 1.2.3.4, but host.com will be forwarded to default upstreams.
**Examples**
......@@ -261,11 +262,16 @@ Sends queries for `*.local` domains to `192.168.0.1:53`. Other queries are sent
./dnsproxy -u 8.8.8.8:53 -u [/local/]192.168.0.1:53
```
Sends queries for `*.host.com` to `1.1.1.1:53` except for `*.maps.host.com` which are sent to `8.8.8.8:53` (as long as other queries).
Sends queries for `*.host.com` to `1.1.1.1:53` except for `*.maps.host.com` which are sent to `8.8.8.8:53` (along with other queries).
```
./dnsproxy -u 8.8.8.8:53 -u [/host.com/]1.1.1.1:53 -u [/maps.host.com/]#
```
Sends queries for `*.host.com` to `1.1.1.1:53` except for `host.com` which is sent to `8.8.8.8:53` (along with other queries).
```
./dnsproxy -u 8.8.8.8:53 -u [/*.host.com/]1.1.1.1:53
```
### EDNS Client Subnet
To enable support for EDNS Client Subnet extension you should run dnsproxy with `--edns` flag:
......
......@@ -6,39 +6,49 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/dnsproxy/upstream"
)
// UpstreamConfig is a wrapper for list of default upstreams and map of reserved domains and corresponding upstreams
type UpstreamConfig struct {
Upstreams []upstream.Upstream // list of default upstreams
DomainReservedUpstreams map[string][]upstream.Upstream // map of reserved domains and lists of corresponding upstreams
Upstreams []upstream.Upstream // list of default upstreams
DomainReservedUpstreams map[string][]upstream.Upstream // map of reserved domains and lists of corresponding upstreams
SpecifiedDomainUpstreams map[string][]upstream.Upstream // map of excluded domains and lists of corresponding upstreams
SubdomainExclusions *stringutil.Set // set of domains with sub-domains exclusions
}
// ParseUpstreamsConfig returns UpstreamConfig and error if upstreams configuration is invalid
// default upstream syntax: <upstreamString>
// reserved upstream syntax: [/domain1/../domainN/]<upstreamString>
// subdomains only upstream syntax: [/*.domain1/../*.domainN]<upstreamString>
// More specific domains take priority over less specific domains,
// To exclude more specific domains from reserved upstreams querying you should use the following syntax: [/domain1/../domainN/]#
// So the following config: ["[/host.com/]1.2.3.4", "[/www.host.com/]2.3.4.5", "[/maps.host.com/]#", "3.4.5.6"]
// will send queries for *.host.com to 1.2.3.4, except for *.www.host.com, which will go to 2.3.4.5 and *.maps.host.com,
// which will go to default server 3.4.5.6 with all other domains
// which will go to default server 3.4.5.6 with all other domains.
// To exclude top level domain from reserved upstreams querying you could use the following: [/*.domain.com/]<upstreamString>
// So the following config: ["[/*.domain.com/]1.2.3.4", "3.4.5.6"] will send queries for all subdomains *.domain.com to 1.2.3.4,
// but domain.com query will be sent to default server 3.4.5.6 as every other query.
func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*UpstreamConfig, error) {
if options == nil {
options = &upstream.Options{}
}
var upstreams []upstream.Upstream
domainReservedUpstreams := map[string][]upstream.Upstream{}
if len(options.Bootstrap) > 0 {
log.Debug("Bootstraps: %v", options.Bootstrap)
}
var upstreams []upstream.Upstream
// We use this index to avoid creating duplicates of upstreams
upstreamsIndex := map[string]upstream.Upstream{}
domainReservedUpstreams := map[string][]upstream.Upstream{}
specifiedDomainUpstreams := map[string][]upstream.Upstream{}
subdomainsOnlyUpstreams := map[string][]upstream.Upstream{}
subdomainsOnlyExclusions := stringutil.NewSet()
for i, l := range upstreamConfig {
u, hosts, err := parseUpstreamLine(l)
if err != nil {
......@@ -49,6 +59,7 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
if u == "#" && len(hosts) > 0 {
for _, host := range hosts {
domainReservedUpstreams[host] = nil
specifiedDomainUpstreams[host] = nil
}
} else {
dnsUpstream, ok := upstreamsIndex[u]
......@@ -61,8 +72,10 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
Timeout: options.Timeout,
InsecureSkipVerify: options.InsecureSkipVerify,
})
if err != nil {
err = fmt.Errorf("cannot prepare the upstream %s (%s): %s", l, options.Bootstrap, err)
return &UpstreamConfig{}, err
}
......@@ -70,25 +83,43 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
upstreamsIndex[u] = dnsUpstream
}
if len(hosts) > 0 {
for _, host := range hosts {
_, ok := domainReservedUpstreams[host]
if !ok {
domainReservedUpstreams[host] = []upstream.Upstream{}
}
domainReservedUpstreams[host] = append(domainReservedUpstreams[host], dnsUpstream)
}
log.Debug("Upstream %d: %s is reserved for next domains: %s",
i, dnsUpstream.Address(), strings.Join(hosts, ", "))
} else {
if len(hosts) == 0 {
log.Debug("Upstream %d: %s", i, dnsUpstream.Address())
upstreams = append(upstreams, dnsUpstream)
continue
}
for _, host := range hosts {
if strings.HasPrefix(host, "*.") {
host = host[len("*."):]
subdomainsOnlyExclusions.Add(host)
log.Debug("domain %s is added to exclusions list", host)
subdomainsOnlyUpstreams[host] = append(subdomainsOnlyUpstreams[host], dnsUpstream)
} else {
specifiedDomainUpstreams[host] = append(specifiedDomainUpstreams[host], dnsUpstream)
}
domainReservedUpstreams[host] = append(domainReservedUpstreams[host], dnsUpstream)
}
log.Debug("Upstream %d: %s is reserved for next domains: %s",
i, dnsUpstream.Address(), strings.Join(hosts, ", "))
}
}
for host, ups := range subdomainsOnlyUpstreams {
// Rewrite ups for wildcard subdomains to remove upper level domains specs.
domainReservedUpstreams[host] = ups
}
return &UpstreamConfig{
Upstreams: upstreams,
DomainReservedUpstreams: domainReservedUpstreams,
Upstreams: upstreams,
DomainReservedUpstreams: domainReservedUpstreams,
SpecifiedDomainUpstreams: specifiedDomainUpstreams,
SubdomainExclusions: subdomainsOnlyExclusions,
}, nil
}
......@@ -108,12 +139,14 @@ func parseUpstreamLine(l string) (string, []string, error) {
}
// split domains list
for _, host := range strings.Split(domainsAndUpstream[0], "/") {
if host != "" {
for _, confHost := range strings.Split(domainsAndUpstream[0], "/") {
if confHost != "" {
host := strings.TrimPrefix(confHost, "*.")
if err := netutil.ValidateDomainName(host); err != nil {
return "", nil, err
}
hosts = append(hosts, strings.ToLower(host+"."))
hosts = append(hosts, strings.ToLower(confHost+"."))
} else {
// empty domain specification means `unqualified names only`
hosts = append(hosts, UnqualifiedNames)
......@@ -141,6 +174,22 @@ func (uc *UpstreamConfig) getUpstreamsForDomain(host string) (ups []upstream.Ups
host = UnqualifiedNames
} else {
host = strings.ToLower(host)
if uc.SubdomainExclusions.Has(host) {
ups, ok := uc.SpecifiedDomainUpstreams[host]
if ok && len(ups) > 0 {
return ups
}
// Check if there is a spec for upper level domain.
h := strings.SplitAfterN(host, ".", 2)
ups, ok = uc.DomainReservedUpstreams[h[1]]
if ok && len(ups) > 0 {
return ups
}
return uc.Upstreams
}
}
for i := 1; i <= dotsCount; i++ {
......
......@@ -27,11 +27,11 @@ func TestGetUpstreamsForDomain(t *testing.T) {
)
require.NoError(t, err)
assertUpstreamsForDomain(t, config, 2, "www.google.com.", []string{"1.2.3.4:53", "tls://1.1.1.1:853"})
assertUpstreamsForDomain(t, config, 1, "www2.google.com.", []string{"4.3.2.1:53"})
assertUpstreamsForDomain(t, config, 1, "internal.local.", []string{"4.3.2.1:53"})
assertUpstreamsForDomain(t, config, 1, "google.", []string{"1.2.3.4:53"})
assertUpstreamsForDomain(t, config, 0, "maps.google.com.", []string{})
assertUpstreamsForDomain(t, config, "www.google.com.", []string{"1.2.3.4:53", "tls://1.1.1.1:853"})
assertUpstreamsForDomain(t, config, "www2.google.com.", []string{"4.3.2.1:53"})
assertUpstreamsForDomain(t, config, "internal.local.", []string{"4.3.2.1:53"})
assertUpstreamsForDomain(t, config, "google.", []string{"1.2.3.4:53"})
assertUpstreamsForDomain(t, config, "maps.google.com.", []string{})
}
func TestGetUpstreamsForDomainWithoutDuplicates(t *testing.T) {
......@@ -55,14 +55,141 @@ func TestGetUpstreamsForDomainWithoutDuplicates(t *testing.T) {
assert.Same(t, u1, u2)
}
func TestGetUpstreamsForDomain_wildcards(t *testing.T) {
conf := []string{
"0.0.0.1",
"[/a.x/]0.0.0.2",
"[/*.a.x/]0.0.0.3",
"[/b.a.x/]0.0.0.4",
"[/*.b.a.x/]0.0.0.5",
"[/*.x.z/]0.0.0.6",
"[/c.b.a.x/]#",
}
uconf, err := ParseUpstreamsConfig(conf, nil)
require.NoError(t, err)
testCases := []struct {
name string
in string
want []string
}{{
name: "default",
in: "d.x.",
want: []string{"0.0.0.1:53"},
}, {
name: "specified_one",
in: "a.x.",
want: []string{"0.0.0.2:53"},
}, {
name: "wildcard",
in: "c.a.x.",
want: []string{"0.0.0.3:53"},
}, {
name: "specified_two",
in: "b.a.x.",
want: []string{"0.0.0.4:53"},
}, {
name: "wildcard_two",
in: "d.b.a.x.",
want: []string{"0.0.0.5:53"},
}, {
name: "specified_three",
in: "c.b.a.x.",
want: []string{"0.0.0.1:53"},
}, {
name: "specified_four",
in: "d.c.b.a.x.",
want: []string{"0.0.0.1:53"},
}, {
name: "unspecified_wildcard",
in: "x.z.",
want: []string{"0.0.0.1:53"},
}, {
name: "unspecified_wildcard_sub",
in: "a.x.z.",
want: []string{"0.0.0.6:53"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assertUpstreamsForDomain(t, uconf, tc.in, tc.want)
})
}
}
func TestGetUpstreamsForDomain_sub_wildcards(t *testing.T) {
conf := []string{
"0.0.0.1",
"[/a.x/]0.0.0.2",
"[/*.a.x/]0.0.0.3",
"[/*.b.a.x/]0.0.0.5",
}
uconf, err := ParseUpstreamsConfig(conf, nil)
require.NoError(t, err)
testCases := []struct {
name string
in string
want []string
}{{
name: "specified",
in: "a.x.",
want: []string{"0.0.0.2:53"},
}, {
name: "wildcard",
in: "c.a.x.",
want: []string{"0.0.0.3:53"},
}, {
name: "sub_spec_ignore",
in: "b.a.x.",
want: []string{"0.0.0.3:53"},
}, {
name: "sub_spec_wildcard",
in: "d.b.a.x.",
want: []string{"0.0.0.5:53"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assertUpstreamsForDomain(t, uconf, tc.in, tc.want)
})
}
}
func BenchmarkGetUpstreamsForDomain(b *testing.B) {
upstreams := []string{
"[/google.com/local/]4.3.2.1",
"[/www.google.com//]1.2.3.4",
"[/maps.google.com/]#",
"[/www.google.com/]tls://1.1.1.1",
}
config, _ := ParseUpstreamsConfig(
upstreams,
&upstream.Options{
InsecureSkipVerify: false,
Bootstrap: []string{},
Timeout: 1 * time.Second,
},
)
for i := 0; i < b.N; i++ {
assertUpstreamsForDomain(b, config, "www.google.com.", []string{"1.2.3.4:53", "tls://1.1.1.1:853"})
assertUpstreamsForDomain(b, config, "www2.google.com.", []string{"4.3.2.1:53"})
assertUpstreamsForDomain(b, config, "internal.local.", []string{"4.3.2.1:53"})
assertUpstreamsForDomain(b, config, "google.", []string{"1.2.3.4:53"})
assertUpstreamsForDomain(b, config, "maps.google.com.", []string{})
}
}
// assertUpstreamsForDomain checks the addresses of the specified domain
// upstreams and their number.
func assertUpstreamsForDomain(t *testing.T, config *UpstreamConfig, count int, domain string, address []string) {
func assertUpstreamsForDomain(t testing.TB, config *UpstreamConfig, domain string, address []string) {
t.Helper()
u := config.getUpstreamsForDomain(domain)
assert.Len(t, u, count)
require.Len(t, u, len(address))
for i, up := range u {
......