Skip to content

Commit 7177922

Browse files
committed
feat: specify skipped domains
1 parent d64332f commit 7177922

File tree

4 files changed

+167
-8
lines changed

4 files changed

+167
-8
lines changed

proxy/proxy.go

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

536+
if p.UpstreamConfig.checkSkipped(host) {
537+
return nil, false, true
538+
}
539+
536540
if d.RequestedPrivateRDNS != (netip.Prefix{}) || p.shouldStripDNS64(d.Req) {
537541
// Use private upstreams.
538542
private := p.PrivateRDNSUpstreamConfig
@@ -541,7 +545,7 @@ func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, i
541545
upstreams = private.getUpstreamsForDomain(host)
542546
}
543547

544-
return upstreams, true
548+
return upstreams, true, false
545549
}
546550

547551
getUpstreams := (*UpstreamConfig).getUpstreamsForDomain
@@ -553,20 +557,27 @@ func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, i
553557
// Try to use custom.
554558
upstreams = getUpstreams(custom.upstream, host)
555559
if len(upstreams) > 0 {
556-
return upstreams, false
560+
return upstreams, false, false
557561
}
558562
}
559563

560564
// Use configured.
561-
return getUpstreams(p.UpstreamConfig, host), false
565+
return getUpstreams(p.UpstreamConfig, host), false, false
562566
}
563567

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

569-
upstreams, isPrivate := p.selectUpstreams(d)
573+
upstreams, isPrivate, isSkipped := p.selectUpstreams(d)
574+
575+
if isSkipped {
576+
p.logger.Debug("skipping domain", "domain", d.Req.Question[0].Name)
577+
d.Res = p.messages.NewMsgNXDOMAIN(req)
578+
return true, nil
579+
}
580+
570581
if len(upstreams) == 0 {
571582
d.Res = p.messages.NewMsgNXDOMAIN(req)
572583

proxy/proxy_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,45 @@ func TestRefuseAny(t *testing.T) {
928928
assert.Equal(t, dns.RcodeNotImplemented, r.Rcode)
929929
}
930930

931+
func TestSkippedDomain(t *testing.T) {
932+
dnsProxy := mustNew(t, &Config{
933+
Logger: slogutil.NewDiscardLogger(),
934+
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
935+
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
936+
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr, "[/google.com/]-"),
937+
TrustedProxies: defaultTrustedProxies,
938+
RatelimitSubnetLenIPv4: 24,
939+
RatelimitSubnetLenIPv6: 64,
940+
RefuseAny: false,
941+
})
942+
943+
// Start listening
944+
ctx := context.Background()
945+
err := dnsProxy.Start(ctx)
946+
require.NoError(t, err)
947+
testutil.CleanupAndRequireSuccess(t, func() (err error) { return dnsProxy.Shutdown(ctx) })
948+
949+
// Create a DNS-over-UDP client connection
950+
addr := dnsProxy.Addr(ProtoUDP)
951+
client := &dns.Client{
952+
Net: string(ProtoUDP),
953+
Timeout: testTimeout,
954+
}
955+
956+
// Create a DNS request
957+
request := (&dns.Msg{
958+
MsgHdr: dns.MsgHdr{
959+
Id: dns.Id(),
960+
RecursionDesired: true,
961+
},
962+
}).SetQuestion("google.com.", dns.TypeANY)
963+
964+
r, _, err := client.Exchange(request, addr.String())
965+
require.NoError(t, err)
966+
967+
assert.Equal(t, dns.RcodeNameError, r.Rcode)
968+
}
969+
931970
func TestInvalidDNSRequest(t *testing.T) {
932971
dnsProxy := mustNew(t, &Config{
933972
Logger: slogutil.NewDiscardLogger(),

proxy/upstreams.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ type UpstreamConfig struct {
2727
// SpecifiedDomainUpstreams maps the specific domain names to the upstreams.
2828
SpecifiedDomainUpstreams map[string][]upstream.Upstream
2929

30+
// SkippedDomains is a set of domains which will never be proxied to any
31+
// upstream server. The dnsproxy will return NXDOMAIN for any of these
32+
// domains or their subdomains.
33+
SkippedDomains *container.MapSet[string]
34+
35+
// SkippedDomainsExclusions is a set of domains which will be proxied to an
36+
// upstream server, regardless of a match in SkippedDomains. This is mainly
37+
// used when a wildcard subdomain is used in the upstream definition (e.g.
38+
// *.my.domain), where my.domain would be proxied to an upstream, but any
39+
// subdomain of it would be skipped.
40+
SkippedDomainsExclusions *container.MapSet[string]
41+
3042
// SubdomainExclusions is set of domains with subdomains exclusions.
3143
SubdomainExclusions *container.MapSet[string]
3244

@@ -63,16 +75,24 @@ var _ io.Closer = (*UpstreamConfig)(nil)
6375
//
6476
// [/domain1/../domainN/]#
6577
//
78+
// Using a hyphen "-" as the <upstreamString> will ensure that the matched
79+
// domain(s) will never be recursed to upstream nameservers. The dnsproxy will
80+
// respond with NXDOMAIN for any matched domain or subdomain. For example:
81+
//
82+
// [/domain1/../domainN/]-
83+
//
6684
// So the following config:
6785
//
6886
// [/host.com/]1.2.3.4
6987
// [/www.host.com/]2.3.4.5"
88+
// [/domain.local/]-
7089
// [/maps.host.com/news.host.com/]#
7190
// 3.4.5.6
7291
//
7392
// will send queries for *.host.com to 1.2.3.4. Except for *.www.host.com,
74-
// which will go to 2.3.4.5. And *.maps.host.com or *.news.host.com, which
75-
// will go to default server 3.4.5.6 with all other domains.
93+
// which will go to 2.3.4.5. Any requests to *.domain.local or domain.local
94+
// will be answered with NXDOMAIN. And *.maps.host.com or *.news.host.com,
95+
// which will go to default server 3.4.5.6 with all other domains.
7696
//
7797
// To exclude top level domain from reserved upstreams querying you could use
7898
// the following:
@@ -108,6 +128,8 @@ func ParseUpstreamsConfig(
108128
domainReservedUpstreams: map[string][]upstream.Upstream{},
109129
specifiedDomainUpstreams: map[string][]upstream.Upstream{},
110130
subdomainsOnlyUpstreams: map[string][]upstream.Upstream{},
131+
skippedDomains: container.NewMapSet[string](),
132+
skippedDomainsExclusions: container.NewMapSet[string](),
111133
subdomainsOnlyExclusions: container.NewMapSet[string](),
112134
}
113135

@@ -161,6 +183,17 @@ type configParser struct {
161183
// corresponding upstreams.
162184
subdomainsOnlyUpstreams map[string][]upstream.Upstream
163185

186+
// skippedDomains is a set of domains which should never be looked up on
187+
// any upstream server.
188+
skippedDomains *container.MapSet[string]
189+
190+
// skippedDomainsExclusions is a set of domains which should be looked up on
191+
// any upstream server, even though it matches an entry in skippedDomains.
192+
// This is mainly used when a wildcard subdomain is used in the upstream
193+
// definition (e.g. *.my.domain), where my.domain would be proxied to an
194+
// upstream, but any subdomain of it would be skipped.
195+
skippedDomainsExclusions *container.MapSet[string]
196+
164197
// subdomainsOnlyExclusions is set of domains with subdomains exclusions.
165198
subdomainsOnlyExclusions *container.MapSet[string]
166199

@@ -188,6 +221,8 @@ func (p *configParser) parse(lines []string) (c *UpstreamConfig, err error) {
188221
DomainReservedUpstreams: p.domainReservedUpstreams,
189222
SpecifiedDomainUpstreams: p.specifiedDomainUpstreams,
190223
SubdomainExclusions: p.subdomainsOnlyExclusions,
224+
SkippedDomains: p.skippedDomains,
225+
SkippedDomainsExclusions: p.skippedDomainsExclusions,
191226
}, errors.Join(errs...)
192227
}
193228

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

241+
if upstreams[0] == "-" && len(domains) > 0 {
242+
p.specifySkipped(domains)
243+
244+
return nil
245+
}
246+
206247
if upstreams[0] == "#" && len(domains) > 0 {
207248
p.excludeFromReserved(domains)
208249

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

297+
func (p *configParser) specifySkipped(domains []string) {
298+
for _, domain := range domains {
299+
if strings.HasPrefix(domain, "*.") {
300+
domain = strings.TrimPrefix(domain, "*.")
301+
p.skippedDomainsExclusions.Add(domain)
302+
}
303+
p.skippedDomains.Add(domain)
304+
}
305+
}
306+
256307
// specifyUpstream specifies the upstream for domains.
257308
func (p *configParser) specifyUpstream(domains []string, u string, idx int) (err error) {
258309
dnsUpstream, ok := p.upstreamsIndex[u]
@@ -373,7 +424,7 @@ func ValidatePrivateConfig(uc *UpstreamConfig, privateSubnets netutil.SubnetSet)
373424

374425
// getUpstreamsForDomain returns the upstreams specified for resolving fqdn. It
375426
// always returns the default set of upstreams if the domain is not reserved for
376-
// any other upstreams.
427+
// any other upstreams. If the domain is skipped, it returns nil.
377428
//
378429
// More specific domains take priority over less specific ones. For example, if
379430
// the upstreams specified for the following domains:
@@ -384,6 +435,10 @@ func ValidatePrivateConfig(uc *UpstreamConfig, privateSubnets netutil.SubnetSet)
384435
// The request for mail.host.com will be resolved using the upstreams specified
385436
// for host.com.
386437
func (uc *UpstreamConfig) getUpstreamsForDomain(fqdn string) (ups []upstream.Upstream) {
438+
if uc.checkSkipped(fqdn) {
439+
return nil
440+
}
441+
387442
if len(uc.DomainReservedUpstreams) == 0 {
388443
return uc.Upstreams
389444
}
@@ -413,6 +468,19 @@ func (uc *UpstreamConfig) getUpstreamsForDomain(fqdn string) (ups []upstream.Ups
413468
return uc.Upstreams
414469
}
415470

471+
func (uc *UpstreamConfig) checkSkipped(host string) bool {
472+
if uc.SkippedDomainsExclusions.Has(host) {
473+
return false
474+
}
475+
for host != "" {
476+
if uc.SkippedDomains.Has(host) {
477+
return true
478+
}
479+
_, host, _ = strings.Cut(host, ".")
480+
}
481+
return false
482+
}
483+
416484
// getUpstreamsForDS is like [getUpstreamsForDomain], but intended for DS
417485
// queries only, so that it matches fqdn without the first label.
418486
//

proxy/upstreams_internal_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const (
3434

3535
wildcardDomain = "*." + firstLevelDomain
3636
anotherSubFQDN = "another." + firstLevelDomain + "."
37+
38+
skippedDomain = "skipped.domain"
39+
skippedFQDN = skippedDomain + "."
3740
)
3841

3942
// Upstream URLs used in tests of [UpstreamConfig].
@@ -57,6 +60,7 @@ var testUpstreamConfigLines = []string{
5760
"[/" + wildcardDomain + "/]" + wildcardUpstream,
5861
"[/" + generalDomain + "/]#",
5962
"[/" + subDomain + "/]" + subdomainUpstream,
63+
"[/" + skippedDomain + "/]-",
6064
}
6165

6266
func TestUpstreamConfig_GetUpstreamsForDomain(t *testing.T) {
@@ -101,6 +105,10 @@ func TestUpstreamConfig_GetUpstreamsForDomain(t *testing.T) {
101105
name: "subdomain",
102106
in: subFQDN,
103107
want: []string{subdomainUpstream},
108+
}, {
109+
name: "skipped",
110+
in: skippedFQDN,
111+
want: nil,
104112
}}
105113

106114
for _, tc := range testCases {
@@ -155,6 +163,10 @@ func TestUpstreamConfig_GetUpstreamsForDS(t *testing.T) {
155163
name: "subdomain",
156164
in: "label." + subFQDN,
157165
want: []string{subdomainUpstream},
166+
}, {
167+
name: "skipped",
168+
in: "label." + skippedFQDN,
169+
want: nil,
158170
}}
159171

160172
for _, tc := range testCases {
@@ -298,6 +310,7 @@ func TestGetUpstreamsForDomain_wildcards(t *testing.T) {
298310
"[/b.a.x/]0.0.0.4",
299311
"[/*.b.a.x/]0.0.0.5",
300312
"[/*.x.z/]0.0.0.6",
313+
"[/*.w.x.z/]-",
301314
"[/c.b.a.x/]#",
302315
}
303316

@@ -344,6 +357,14 @@ func TestGetUpstreamsForDomain_wildcards(t *testing.T) {
344357
name: "unspecified_wildcard_sub",
345358
in: "a.x.z.",
346359
want: []string{"0.0.0.6:53"},
360+
}, {
361+
name: "skipped",
362+
in: "a.w.x.z.",
363+
want: nil,
364+
}, {
365+
name: "skipped_sub",
366+
in: "a.b.w.x.z.",
367+
want: nil,
347368
}}
348369

349370
for _, tc := range testCases {
@@ -402,6 +423,8 @@ func TestGetUpstreamsForDomain_default_wildcards(t *testing.T) {
402423
"[/*.example.org/]127.0.0.1:5303",
403424
"[/www.example.org/]127.0.0.1:5304",
404425
"[/*.www.example.org/]#",
426+
"[/skipped.www.example.org/]-",
427+
"[/*.skipped.example.org/]-",
405428
}
406429

407430
uconf, err := ParseUpstreamsConfig(conf, nil)
@@ -427,6 +450,22 @@ func TestGetUpstreamsForDomain_default_wildcards(t *testing.T) {
427450
name: "def_wildcard",
428451
in: "abc.www.example.org.",
429452
want: []string{"127.0.0.1:5301"},
453+
}, {
454+
name: "skipped",
455+
in: "skipped.www.example.org.",
456+
want: nil,
457+
}, {
458+
name: "skipped_sub",
459+
in: "sub.skipped.www.example.org.",
460+
want: nil,
461+
}, {
462+
name: "skipped_wildcard",
463+
in: "sub.skipped.example.org.",
464+
want: nil,
465+
}, {
466+
name: "skipped_wildcard_parent",
467+
in: "skipped.example.org.",
468+
want: []string{"127.0.0.1:5303"},
430469
}}
431470

432471
for _, tc := range testCases {
@@ -442,6 +481,7 @@ func BenchmarkGetUpstreamsForDomain(b *testing.B) {
442481
"[/google.com/local/]4.3.2.1",
443482
"[/www.google.com//]1.2.3.4",
444483
"[/maps.google.com/]#",
484+
"[/skipped.google.com/]-",
445485
"[/www.google.com/]tls://1.1.1.1",
446486
"192.0.2.1",
447487
}
@@ -459,6 +499,7 @@ func BenchmarkGetUpstreamsForDomain(b *testing.B) {
459499
"internal.local.",
460500
"google.",
461501
"maps.google.com.",
502+
"skipped.google.com.",
462503
}
463504

464505
var upstreams []upstream.Upstream

0 commit comments

Comments
 (0)