Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ 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", which will be used to resolve names with a single label in them, or with exactly two labels in case of `DS` requests.
1. 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`.
1. The special server address `#` means, "use the common 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.
1. The special server address `-` means "skip, do not proxy the request, return NXDOMAIN", so: `--upstream=1.2.3.4 --upstream=[/host.com/]-` will respond with NXDOMAIN for any queries for `host.com` or its subdomains (and will not forward the request to any upstream), but all other requests will be forwarded to the nameserver at `1.2.3.4` as usual.
1. 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.

Sends requests for `*.local` domains to `192.168.0.1:53`. Other requests are sent to `8.8.8.8:53`:
Expand Down Expand Up @@ -348,13 +349,14 @@ Sends requests for `*.host.com` to `1.1.1.1:53` except for `host.com` which is s
;
```

Sends requests for `com` (and its subdomains) to `1.2.3.4:53`, requests for other top-level domains to `1.1.1.1:53`, and all other requests to `8.8.8.8:53`:
Respond with NXDOMAIN for requests for `mydomain.com` (and its subdomains), send requests for `com` (and its subdomains) to `1.2.3.4:53`, requests for other top-level domains to `1.1.1.1:53`, and all other requests to `8.8.8.8:53`:

```shell
./dnsproxy \
-u "8.8.8.8:53" \
-u "[//]1.1.1.1:53" \
-u "[/com/]1.2.3.4:53" \
-u "[/mydomain.com/]-" \
;
```

Expand Down
21 changes: 16 additions & 5 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,14 @@ func (p *Proxy) Addr(proto Proto) (addr net.Addr) {
// selectUpstreams returns the upstreams to use for the specified host. It
// firstly considers custom upstreams if those aren't empty and then the
// configured ones. The returned slice may be empty or nil.
func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, isPrivate bool) {
func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, isPrivate bool, isSkipped bool) {
q := d.Req.Question[0]
host := q.Name

if p.UpstreamConfig.checkSkipped(host) {
return nil, false, true
}

if d.RequestedPrivateRDNS != (netip.Prefix{}) || p.shouldStripDNS64(d.Req) {
// Use private upstreams.
private := p.PrivateRDNSUpstreamConfig
Expand All @@ -541,7 +545,7 @@ func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, i
upstreams = private.getUpstreamsForDomain(host)
}

return upstreams, true
return upstreams, true, false
}

getUpstreams := (*UpstreamConfig).getUpstreamsForDomain
Expand All @@ -553,20 +557,27 @@ func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, i
// Try to use custom.
upstreams = getUpstreams(custom.upstream, host)
if len(upstreams) > 0 {
return upstreams, false
return upstreams, false, false
}
}

// Use configured.
return getUpstreams(p.UpstreamConfig, host), false
return getUpstreams(p.UpstreamConfig, host), false, false
}

// replyFromUpstream tries to resolve the request via configured upstream
// servers. It returns true if the response actually came from an upstream.
func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) {
req := d.Req

upstreams, isPrivate := p.selectUpstreams(d)
upstreams, isPrivate, isSkipped := p.selectUpstreams(d)

if isSkipped {
p.logger.Debug("skipping domain", "domain", d.Req.Question[0].Name)
d.Res = p.messages.NewMsgNXDOMAIN(req)
return true, nil
}

if len(upstreams) == 0 {
d.Res = p.messages.NewMsgNXDOMAIN(req)

Expand Down
39 changes: 39 additions & 0 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,45 @@ func TestRefuseAny(t *testing.T) {
assert.Equal(t, dns.RcodeNotImplemented, r.Rcode)
}

func TestSkippedDomain(t *testing.T) {
dnsProxy := mustNew(t, &Config{
Logger: slogutil.NewDiscardLogger(),
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr, "[/google.com/]-"),
TrustedProxies: defaultTrustedProxies,
RatelimitSubnetLenIPv4: 24,
RatelimitSubnetLenIPv6: 64,
RefuseAny: false,
})

// Start listening
ctx := context.Background()
err := dnsProxy.Start(ctx)
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) { return dnsProxy.Shutdown(ctx) })

// Create a DNS-over-UDP client connection
addr := dnsProxy.Addr(ProtoUDP)
client := &dns.Client{
Net: string(ProtoUDP),
Timeout: testTimeout,
}

// Create a DNS request
request := (&dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
}).SetQuestion("google.com.", dns.TypeANY)

r, _, err := client.Exchange(request, addr.String())
require.NoError(t, err)

assert.Equal(t, dns.RcodeNameError, r.Rcode)
}

func TestInvalidDNSRequest(t *testing.T) {
dnsProxy := mustNew(t, &Config{
Logger: slogutil.NewDiscardLogger(),
Expand Down
74 changes: 71 additions & 3 deletions proxy/upstreams.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ type UpstreamConfig struct {
// SpecifiedDomainUpstreams maps the specific domain names to the upstreams.
SpecifiedDomainUpstreams map[string][]upstream.Upstream

// SkippedDomains is a set of domains which will never be proxied to any
// upstream server. The dnsproxy will return NXDOMAIN for any of these
// domains or their subdomains.
SkippedDomains *container.MapSet[string]

// SkippedDomainsExclusions is a set of domains which will be proxied to an
// upstream server, regardless of a match in SkippedDomains. This is mainly
// used when a wildcard subdomain is used in the upstream definition (e.g.
// *.my.domain), where my.domain would be proxied to an upstream, but any
// subdomain of it would be skipped.
SkippedDomainsExclusions *container.MapSet[string]

// SubdomainExclusions is set of domains with subdomains exclusions.
SubdomainExclusions *container.MapSet[string]

Expand Down Expand Up @@ -63,16 +75,24 @@ var _ io.Closer = (*UpstreamConfig)(nil)
//
// [/domain1/../domainN/]#
//
// Using a hyphen "-" as the <upstreamString> will ensure that the matched
// domain(s) will never be recursed to upstream nameservers. The dnsproxy will
// respond with NXDOMAIN for any matched domain or subdomain. For example:
//
// [/domain1/../domainN/]-
//
// So the following config:
//
// [/host.com/]1.2.3.4
// [/www.host.com/]2.3.4.5"
// [/domain.local/]-
// [/maps.host.com/news.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 or *.news.host.com, which
// will go to default server 3.4.5.6 with all other domains.
// which will go to 2.3.4.5. Any requests to *.domain.local or domain.local
// will be answered with NXDOMAIN. And *.maps.host.com or *.news.host.com,
// 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:
Expand Down Expand Up @@ -108,6 +128,8 @@ func ParseUpstreamsConfig(
domainReservedUpstreams: map[string][]upstream.Upstream{},
specifiedDomainUpstreams: map[string][]upstream.Upstream{},
subdomainsOnlyUpstreams: map[string][]upstream.Upstream{},
skippedDomains: container.NewMapSet[string](),
skippedDomainsExclusions: container.NewMapSet[string](),
subdomainsOnlyExclusions: container.NewMapSet[string](),
}

Expand Down Expand Up @@ -161,6 +183,17 @@ type configParser struct {
// corresponding upstreams.
subdomainsOnlyUpstreams map[string][]upstream.Upstream

// skippedDomains is a set of domains which should never be looked up on
// any upstream server.
skippedDomains *container.MapSet[string]

// skippedDomainsExclusions is a set of domains which should be looked up on
// any upstream server, even though it matches an entry in skippedDomains.
// This is mainly used when a wildcard subdomain is used in the upstream
// definition (e.g. *.my.domain), where my.domain would be proxied to an
// upstream, but any subdomain of it would be skipped.
skippedDomainsExclusions *container.MapSet[string]

// subdomainsOnlyExclusions is set of domains with subdomains exclusions.
subdomainsOnlyExclusions *container.MapSet[string]

Expand Down Expand Up @@ -188,6 +221,8 @@ func (p *configParser) parse(lines []string) (c *UpstreamConfig, err error) {
DomainReservedUpstreams: p.domainReservedUpstreams,
SpecifiedDomainUpstreams: p.specifiedDomainUpstreams,
SubdomainExclusions: p.subdomainsOnlyExclusions,
SkippedDomains: p.skippedDomains,
SkippedDomainsExclusions: p.skippedDomainsExclusions,
}, errors.Join(errs...)
}

Expand All @@ -203,6 +238,12 @@ func (p *configParser) parseLine(idx int, confLine string) (err error) {
return err
}

if upstreams[0] == "-" && len(domains) > 0 {
p.specifySkipped(domains)

return nil
}

if upstreams[0] == "#" && len(domains) > 0 {
p.excludeFromReserved(domains)

Expand Down Expand Up @@ -253,6 +294,16 @@ func splitConfigLine(confLine string) (upstreams, domains []string, err error) {
return strings.Fields(upstreamsLine), domains, nil
}

func (p *configParser) specifySkipped(domains []string) {
for _, domain := range domains {
if strings.HasPrefix(domain, "*.") {
domain = strings.TrimPrefix(domain, "*.")
p.skippedDomainsExclusions.Add(domain)
}
p.skippedDomains.Add(domain)
}
}

// specifyUpstream specifies the upstream for domains.
func (p *configParser) specifyUpstream(domains []string, u string, idx int) (err error) {
dnsUpstream, ok := p.upstreamsIndex[u]
Expand Down Expand Up @@ -373,7 +424,7 @@ func ValidatePrivateConfig(uc *UpstreamConfig, privateSubnets netutil.SubnetSet)

// getUpstreamsForDomain returns the upstreams specified for resolving fqdn. It
// always returns the default set of upstreams if the domain is not reserved for
// any other upstreams.
// any other upstreams. If the domain is skipped, it returns nil.
//
// More specific domains take priority over less specific ones. For example, if
// the upstreams specified for the following domains:
Expand All @@ -384,6 +435,10 @@ func ValidatePrivateConfig(uc *UpstreamConfig, privateSubnets netutil.SubnetSet)
// The request for mail.host.com will be resolved using the upstreams specified
// for host.com.
func (uc *UpstreamConfig) getUpstreamsForDomain(fqdn string) (ups []upstream.Upstream) {
if uc.checkSkipped(fqdn) {
return nil
}

if len(uc.DomainReservedUpstreams) == 0 {
return uc.Upstreams
}
Expand Down Expand Up @@ -413,6 +468,19 @@ func (uc *UpstreamConfig) getUpstreamsForDomain(fqdn string) (ups []upstream.Ups
return uc.Upstreams
}

func (uc *UpstreamConfig) checkSkipped(host string) bool {
if uc.SkippedDomainsExclusions.Has(host) {
return false
}
for host != "" {
if uc.SkippedDomains.Has(host) {
return true
}
_, host, _ = strings.Cut(host, ".")
}
return false
}

// getUpstreamsForDS is like [getUpstreamsForDomain], but intended for DS
// queries only, so that it matches fqdn without the first label.
//
Expand Down
41 changes: 41 additions & 0 deletions proxy/upstreams_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const (

wildcardDomain = "*." + firstLevelDomain
anotherSubFQDN = "another." + firstLevelDomain + "."

skippedDomain = "skipped.domain"
skippedFQDN = skippedDomain + "."
)

// Upstream URLs used in tests of [UpstreamConfig].
Expand All @@ -57,6 +60,7 @@ var testUpstreamConfigLines = []string{
"[/" + wildcardDomain + "/]" + wildcardUpstream,
"[/" + generalDomain + "/]#",
"[/" + subDomain + "/]" + subdomainUpstream,
"[/" + skippedDomain + "/]-",
}

func TestUpstreamConfig_GetUpstreamsForDomain(t *testing.T) {
Expand Down Expand Up @@ -101,6 +105,10 @@ func TestUpstreamConfig_GetUpstreamsForDomain(t *testing.T) {
name: "subdomain",
in: subFQDN,
want: []string{subdomainUpstream},
}, {
name: "skipped",
in: skippedFQDN,
want: nil,
}}

for _, tc := range testCases {
Expand Down Expand Up @@ -155,6 +163,10 @@ func TestUpstreamConfig_GetUpstreamsForDS(t *testing.T) {
name: "subdomain",
in: "label." + subFQDN,
want: []string{subdomainUpstream},
}, {
name: "skipped",
in: "label." + skippedFQDN,
want: nil,
}}

for _, tc := range testCases {
Expand Down Expand Up @@ -298,6 +310,7 @@ func TestGetUpstreamsForDomain_wildcards(t *testing.T) {
"[/b.a.x/]0.0.0.4",
"[/*.b.a.x/]0.0.0.5",
"[/*.x.z/]0.0.0.6",
"[/*.w.x.z/]-",
"[/c.b.a.x/]#",
}

Expand Down Expand Up @@ -344,6 +357,14 @@ func TestGetUpstreamsForDomain_wildcards(t *testing.T) {
name: "unspecified_wildcard_sub",
in: "a.x.z.",
want: []string{"0.0.0.6:53"},
}, {
name: "skipped",
in: "a.w.x.z.",
want: nil,
}, {
name: "skipped_sub",
in: "a.b.w.x.z.",
want: nil,
}}

for _, tc := range testCases {
Expand Down Expand Up @@ -402,6 +423,8 @@ func TestGetUpstreamsForDomain_default_wildcards(t *testing.T) {
"[/*.example.org/]127.0.0.1:5303",
"[/www.example.org/]127.0.0.1:5304",
"[/*.www.example.org/]#",
"[/skipped.www.example.org/]-",
"[/*.skipped.example.org/]-",
}

uconf, err := ParseUpstreamsConfig(conf, nil)
Expand All @@ -427,6 +450,22 @@ func TestGetUpstreamsForDomain_default_wildcards(t *testing.T) {
name: "def_wildcard",
in: "abc.www.example.org.",
want: []string{"127.0.0.1:5301"},
}, {
name: "skipped",
in: "skipped.www.example.org.",
want: nil,
}, {
name: "skipped_sub",
in: "sub.skipped.www.example.org.",
want: nil,
}, {
name: "skipped_wildcard",
in: "sub.skipped.example.org.",
want: nil,
}, {
name: "skipped_wildcard_parent",
in: "skipped.example.org.",
want: []string{"127.0.0.1:5303"},
}}

for _, tc := range testCases {
Expand All @@ -442,6 +481,7 @@ func BenchmarkGetUpstreamsForDomain(b *testing.B) {
"[/google.com/local/]4.3.2.1",
"[/www.google.com//]1.2.3.4",
"[/maps.google.com/]#",
"[/skipped.google.com/]-",
"[/www.google.com/]tls://1.1.1.1",
"192.0.2.1",
}
Expand All @@ -459,6 +499,7 @@ func BenchmarkGetUpstreamsForDomain(b *testing.B) {
"internal.local.",
"google.",
"maps.google.com.",
"skipped.google.com.",
}

var upstreams []upstream.Upstream
Expand Down