Pull request: filtering: fix legacy rewrite wildcards

Updates #3343.

Squashed commit of the following:

commit ab3c3e002a6d2a11bc3207fdaaeb292aaa194907
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Jul 12 20:58:57 2021 +0300

    filtering: fix legacy rewrite wildcards
This commit is contained in:
Ainar Garipov 2021-07-12 21:10:39 +03:00
parent f419896ec6
commit 194ea6ef95
4 changed files with 112 additions and 61 deletions

View File

@ -62,6 +62,7 @@ released by then.
### Fixed ### Fixed
- Conflicts between IPv4 and IPv6 DNS rewrites ([#3343]).
- Letter case mismatches in `CNAME` filtering ([#3335]). - Letter case mismatches in `CNAME` filtering ([#3335]).
- Occasional breakages on network errors with DNS-over-HTTP upstreams ([#3217]). - Occasional breakages on network errors with DNS-over-HTTP upstreams ([#3217]).
- Errors when setting static IP on Linux ([#3257]). - Errors when setting static IP on Linux ([#3257]).
@ -103,6 +104,7 @@ released by then.
[#3256]: https://github.com/AdguardTeam/AdGuardHome/issues/3256 [#3256]: https://github.com/AdguardTeam/AdGuardHome/issues/3256
[#3257]: https://github.com/AdguardTeam/AdGuardHome/issues/3257 [#3257]: https://github.com/AdguardTeam/AdGuardHome/issues/3257
[#3335]: https://github.com/AdguardTeam/AdGuardHome/issues/3335 [#3335]: https://github.com/AdguardTeam/AdGuardHome/issues/3335
[#3343]: https://github.com/AdguardTeam/AdGuardHome/issues/3343

View File

@ -492,7 +492,7 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
d.confLock.RLock() d.confLock.RLock()
defer d.confLock.RUnlock() defer d.confLock.RUnlock()
rr := findRewrites(d.Rewrites, host) rr := findRewrites(d.Rewrites, host, qtype)
if len(rr) != 0 { if len(rr) != 0 {
res.Reason = Rewritten res.Reason = Rewritten
} }
@ -517,15 +517,14 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
cnames.Add(host) cnames.Add(host)
res.CanonName = rr[0].Answer res.CanonName = rr[0].Answer
rr = findRewrites(d.Rewrites, host) rr = findRewrites(d.Rewrites, host, qtype)
} }
for _, r := range rr { for _, r := range rr {
if (r.Type == dns.TypeA && qtype == dns.TypeA) || if r.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
(r.Type == dns.TypeAAAA && qtype == dns.TypeAAAA) {
if r.IP == nil { // IP exception if r.IP == nil { // IP exception
res.Reason = 0 res.Reason = NotFilteredNotFound
return res return res
} }

View File

@ -15,10 +15,16 @@ import (
// RewriteEntry is a rewrite array element // RewriteEntry is a rewrite array element
type RewriteEntry struct { type RewriteEntry struct {
// Domain is the domain for which this rewrite should work.
Domain string `yaml:"domain"` Domain string `yaml:"domain"`
Answer string `yaml:"answer"` // IP address or canonical name // Answer is the IP address, canonical name, or one of the special
Type uint16 `yaml:"-"` // DNS record type: CNAME, A or AAAA // values: "A" or "AAAA".
IP net.IP `yaml:"-"` // Parsed IP address (if Type is A or AAAA) Answer string `yaml:"answer"`
// IP is the IP address that should be used in the response if Type is
// A or AAAA.
IP net.IP `yaml:"-"`
// Type is the DNS record type: A, AAAA, or CNAME.
Type uint16 `yaml:"-"`
} }
func (r *RewriteEntry) equals(b RewriteEntry) bool { func (r *RewriteEntry) equals(b RewriteEntry) bool {
@ -26,27 +32,32 @@ func (r *RewriteEntry) equals(b RewriteEntry) bool {
} }
func isWildcard(host string) bool { func isWildcard(host string) bool {
return len(host) >= 2 && return len(host) > 1 && host[0] == '*' && host[1] == '.'
host[0] == '*' && host[1] == '.'
} }
// Return TRUE of host name matches a wildcard pattern // matchDomainWildcard returns true if host matches the wildcard pattern.
func matchDomainWildcard(host, wildcard string) bool { func matchDomainWildcard(host, wildcard string) (ok bool) {
return isWildcard(wildcard) && return isWildcard(wildcard) && strings.HasSuffix(host, wildcard[1:])
strings.HasSuffix(host, wildcard[1:])
} }
type rewritesArray []RewriteEntry // rewritesSorted is a slice of legacy rewrites for sorting.
//
// The sorting priority:
//
// A and AAAA > CNAME
// wildcard > exact
// lower level wildcard > higher level wildcard
//
type rewritesSorted []RewriteEntry
func (a rewritesArray) Len() int { return len(a) } // Len implements the sort.Interface interface for legacyRewritesSorted.
func (a rewritesSorted) Len() int { return len(a) }
func (a rewritesArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } // Swap implements the sort.Interface interface for legacyRewritesSorted.
func (a rewritesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Priority: // Less implements the sort.Interface interface for legacyRewritesSorted.
// . CNAME < A/AAAA; func (a rewritesSorted) Less(i, j int) bool {
// . exact < wildcard;
// . higher level wildcard < lower level wildcard
func (a rewritesArray) Less(i, j int) bool {
if a[i].Type == dns.TypeCNAME && a[j].Type != dns.TypeCNAME { if a[i].Type == dns.TypeCNAME && a[j].Type != dns.TypeCNAME {
return true return true
} else if a[i].Type != dns.TypeCNAME && a[j].Type == dns.TypeCNAME { } else if a[i].Type != dns.TypeCNAME && a[j].Type == dns.TypeCNAME {
@ -67,31 +78,37 @@ func (a rewritesArray) Less(i, j int) bool {
return len(a[i].Domain) > len(a[j].Domain) return len(a[i].Domain) > len(a[j].Domain)
} }
// Prepare entry for use // prepare prepares the a new or decoded entry.
func (r *RewriteEntry) prepare() { func (r *RewriteEntry) prepare() {
if r.Answer == "AAAA" { switch r.Answer {
case "AAAA":
r.IP = nil r.IP = nil
r.Type = dns.TypeAAAA r.Type = dns.TypeAAAA
return return
} else if r.Answer == "A" { case "A":
r.IP = nil r.IP = nil
r.Type = dns.TypeA r.Type = dns.TypeA
return return
default:
// Go on.
} }
ip := net.ParseIP(r.Answer) ip := net.ParseIP(r.Answer)
if ip == nil { if ip == nil {
r.Type = dns.TypeCNAME r.Type = dns.TypeCNAME
return return
} }
r.IP = ip
r.Type = dns.TypeAAAA
ip4 := ip.To4() ip4 := ip.To4()
if ip4 != nil { if ip4 != nil {
r.IP = ip4 r.IP = ip4
r.Type = dns.TypeA r.Type = dns.TypeA
} else {
r.IP = ip
r.Type = dns.TypeAAAA
} }
} }
@ -101,19 +118,23 @@ func (d *DNSFilter) prepareRewrites() {
} }
} }
// Get the list of matched rewrite entries. // findRewrites returns the list of matched rewrite entries. The priority is:
// Priority: CNAME, A/AAAA; exact, wildcard. // CNAME, then A and AAAA; exact, then wildcard. If the host is matched
// If matched exactly, don't return wildcard entries. // exactly, wildcard entries aren't returned. If the host matched by wildcards,
// If matched by several wildcards, select the more specific one // return the most specific for the question type.
func findRewrites(a []RewriteEntry, host string) []RewriteEntry { func findRewrites(a []RewriteEntry, host string, qtype uint16) []RewriteEntry {
rr := rewritesArray{} rr := rewritesSorted{}
for _, r := range a { for _, r := range a {
if r.Domain != host { if r.Domain != host && !matchDomainWildcard(host, r.Domain) {
if !matchDomainWildcard(host, r.Domain) { continue
continue }
}
// Return CNAMEs for all types requests, but only the
// appropriate ones for A and AAAA.
if r.Type == dns.TypeCNAME ||
(r.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA)) {
rr = append(rr, r)
} }
rr = append(rr, r)
} }
if len(rr) == 0 { if len(rr) == 0 {

View File

@ -57,59 +57,75 @@ func TestRewrites(t *testing.T) {
}, { }, {
Domain: "a.host3.com", Domain: "a.host3.com",
Answer: "x.host.com", Answer: "x.host.com",
}, {
Domain: "*.hostboth.com",
Answer: "1.2.3.6",
}, {
Domain: "*.hostboth.com",
Answer: "1234::5678",
}} }}
d.prepareRewrites() d.prepareRewrites()
testCases := []struct { testCases := []struct {
name string name string
host string host string
dtyp uint16
wantCName string wantCName string
wantVals []net.IP wantVals []net.IP
dtyp uint16
}{{ }{{
name: "not_filtered_not_found", name: "not_filtered_not_found",
host: "hoost.com", host: "hoost.com",
dtyp: dns.TypeA, wantCName: "",
wantVals: nil,
dtyp: dns.TypeA,
}, { }, {
name: "rewritten_a", name: "rewritten_a",
host: "www.host.com", host: "www.host.com",
dtyp: dns.TypeA,
wantCName: "host.com", wantCName: "host.com",
wantVals: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 5}}, wantVals: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 5}},
dtyp: dns.TypeA,
}, { }, {
name: "rewritten_aaaa", name: "rewritten_aaaa",
host: "www.host.com", host: "www.host.com",
dtyp: dns.TypeAAAA,
wantCName: "host.com", wantCName: "host.com",
wantVals: []net.IP{net.ParseIP("1:2:3::4")}, wantVals: []net.IP{net.ParseIP("1:2:3::4")},
dtyp: dns.TypeAAAA,
}, { }, {
name: "wildcard_match", name: "wildcard_match",
host: "abc.host.com", host: "abc.host.com",
dtyp: dns.TypeA, wantCName: "",
wantVals: []net.IP{{1, 2, 3, 5}}, wantVals: []net.IP{{1, 2, 3, 5}},
dtyp: dns.TypeA,
}, { }, {
name: "wildcard_override", name: "wildcard_override",
host: "a.host.com", host: "a.host.com",
dtyp: dns.TypeA, wantCName: "",
wantVals: []net.IP{{1, 2, 3, 4}}, wantVals: []net.IP{{1, 2, 3, 4}},
dtyp: dns.TypeA,
}, { }, {
name: "wildcard_cname_interaction", name: "wildcard_cname_interaction",
host: "www.host2.com", host: "www.host2.com",
dtyp: dns.TypeA,
wantCName: "host.com", wantCName: "host.com",
wantVals: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 5}}, wantVals: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 5}},
dtyp: dns.TypeA,
}, { }, {
name: "two_cnames", name: "two_cnames",
host: "b.host.com", host: "b.host.com",
dtyp: dns.TypeA,
wantCName: "somehost.com", wantCName: "somehost.com",
wantVals: []net.IP{{0, 0, 0, 0}}, wantVals: []net.IP{{0, 0, 0, 0}},
dtyp: dns.TypeA,
}, { }, {
name: "two_cnames_and_wildcard", name: "two_cnames_and_wildcard",
host: "b.host3.com", host: "b.host3.com",
dtyp: dns.TypeA,
wantCName: "x.host.com", wantCName: "x.host.com",
wantVals: []net.IP{{1, 2, 3, 5}}, wantVals: []net.IP{{1, 2, 3, 5}},
dtyp: dns.TypeA,
}, {
name: "issue3343",
host: "www.hostboth.com",
wantCName: "",
wantVals: []net.IP{net.ParseIP("1234::5678")},
dtyp: dns.TypeAAAA,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@ -143,12 +159,15 @@ func TestRewritesLevels(t *testing.T) {
d.Rewrites = []RewriteEntry{{ d.Rewrites = []RewriteEntry{{
Domain: "host.com", Domain: "host.com",
Answer: "1.1.1.1", Answer: "1.1.1.1",
Type: dns.TypeA,
}, { }, {
Domain: "*.host.com", Domain: "*.host.com",
Answer: "2.2.2.2", Answer: "2.2.2.2",
Type: dns.TypeA,
}, { }, {
Domain: "*.sub.host.com", Domain: "*.sub.host.com",
Answer: "3.3.3.3", Answer: "3.3.3.3",
Type: dns.TypeA,
}} }}
d.prepareRewrites() d.prepareRewrites()
@ -234,53 +253,61 @@ func TestRewritesExceptionIP(t *testing.T) {
d.Rewrites = []RewriteEntry{{ d.Rewrites = []RewriteEntry{{
Domain: "host.com", Domain: "host.com",
Answer: "1.2.3.4", Answer: "1.2.3.4",
Type: dns.TypeA,
}, { }, {
Domain: "host.com", Domain: "host.com",
Answer: "AAAA", Answer: "AAAA",
Type: dns.TypeAAAA,
}, { }, {
Domain: "host2.com", Domain: "host2.com",
Answer: "::1", Answer: "::1",
Type: dns.TypeAAAA,
}, { }, {
Domain: "host2.com", Domain: "host2.com",
Answer: "A", Answer: "A",
Type: dns.TypeA,
}, { }, {
Domain: "host3.com", Domain: "host3.com",
Answer: "A", Answer: "A",
Type: dns.TypeA,
}} }}
d.prepareRewrites() d.prepareRewrites()
testCases := []struct { testCases := []struct {
name string name string
host string host string
dtyp uint16
want []net.IP want []net.IP
dtyp uint16
}{{ }{{
name: "match_A", name: "match_A",
host: "host.com", host: "host.com",
dtyp: dns.TypeA,
want: []net.IP{{1, 2, 3, 4}}, want: []net.IP{{1, 2, 3, 4}},
dtyp: dns.TypeA,
}, { }, {
name: "exception_AAAA_host.com", name: "exception_AAAA_host.com",
host: "host.com", host: "host.com",
want: nil,
dtyp: dns.TypeAAAA, dtyp: dns.TypeAAAA,
}, { }, {
name: "exception_A_host2.com", name: "exception_A_host2.com",
host: "host2.com", host: "host2.com",
want: nil,
dtyp: dns.TypeA, dtyp: dns.TypeA,
}, { }, {
name: "match_AAAA_host2.com", name: "match_AAAA_host2.com",
host: "host2.com", host: "host2.com",
dtyp: dns.TypeAAAA,
want: []net.IP{net.ParseIP("::1")}, want: []net.IP{net.ParseIP("::1")},
dtyp: dns.TypeAAAA,
}, { }, {
name: "exception_A_host3.com", name: "exception_A_host3.com",
host: "host3.com", host: "host3.com",
want: nil,
dtyp: dns.TypeA, dtyp: dns.TypeA,
}, { }, {
name: "match_AAAA_host3.com", name: "match_AAAA_host3.com",
host: "host3.com", host: "host3.com",
want: nil,
dtyp: dns.TypeAAAA, dtyp: dns.TypeAAAA,
want: []net.IP{},
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@ -293,7 +320,9 @@ func TestRewritesExceptionIP(t *testing.T) {
} }
assert.Equal(t, Rewritten, r.Reason) assert.Equal(t, Rewritten, r.Reason)
require.Len(t, r.IPList, len(tc.want)) require.Len(t, r.IPList, len(tc.want))
for _, ip := range tc.want { for _, ip := range tc.want {
assert.True(t, ip.Equal(r.IPList[0])) assert.True(t, ip.Equal(r.IPList[0]))
} }