Skip to content

Commit

Permalink
filtering: fix rw to subdomain
Browse files Browse the repository at this point in the history
  • Loading branch information
ainar-g committed Dec 27, 2021
1 parent 52f36f2 commit 9213fd8
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 69 deletions.
2 changes: 1 addition & 1 deletion internal/dnsforward/dnsforward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {

func TestRewrite(t *testing.T) {
c := &filtering.Config{
Rewrites: []filtering.RewriteEntry{{
Rewrites: []*filtering.LegacyRewrite{{
Domain: "test.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
Expand Down
37 changes: 29 additions & 8 deletions internal/filtering/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type Config struct {
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)

Rewrites []RewriteEntry `yaml:"rewrites"`
Rewrites []*LegacyRewrite `yaml:"rewrites"`

// Names of services to block (globally).
// Per-client settings can override this configuration.
Expand Down Expand Up @@ -281,8 +281,14 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
c.Rewrites = cloneRewrites(c.Rewrites)
}

func cloneRewrites(entries []RewriteEntry) (clone []RewriteEntry) {
return append([]RewriteEntry(nil), entries...)
// cloneRewrites returns a deep copy of entries.
func cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {
clone = make([]*LegacyRewrite, len(entries))
for i, rw := range entries {
clone[i] = rw.clone()
}

return clone
}

// SetFilters - set new filters (synchronously or asynchronously)
Expand Down Expand Up @@ -530,15 +536,24 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
cnames := stringutil.NewSet()
origHost := host
for matched && len(rewrites) > 0 && rewrites[0].Type == dns.TypeCNAME {
rwAns := rewrites[0].Answer
rw := rewrites[0]
rwPat := rw.Domain
rwAns := rw.Answer

log.Debug("rewrite: cname for %s is %s", host, rwAns)

if host == rwAns {
// Rewrite of a domain onto itself is an exception rule.
if rwPat == rwAns {
// Rewrite of a pattern onto itself is an exception rule. Return
// a not filtered result.
res.Reason = NotFilteredNotFound

return res
} else if host == rwAns && isWildcard(rwPat) {
// An "*.example.com → sub.example.com" rewrite matching in a loop.
// Break the loop.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.
break
}

host = rwAns
Expand All @@ -560,7 +575,7 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {

// setRewriteResult sets the Reason or IPList of res if necessary. res must not
// be nil.
func setRewriteResult(res *Result, host string, rewrites []RewriteEntry, qtype uint16) {
func setRewriteResult(res *Result, host string, rewrites []*LegacyRewrite, qtype uint16) {
for _, rw := range rewrites {
if rw.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
if rw.IP == nil {
Expand Down Expand Up @@ -932,12 +947,18 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
err := d.initSecurityServices()
if err != nil {
log.Error("filtering: initialize services: %s", err)

return nil
}

if c != nil {
d.Config = *c
d.prepareRewrites()
err = d.prepareRewrites()
if err != nil {
log.Error("rewrites: preparing: %s", err)

return nil
}
}

bsvcs := []string{}
Expand Down
142 changes: 91 additions & 51 deletions internal/filtering/rewrites.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,97 +4,121 @@ package filtering

import (
"encoding/json"
"fmt"
"net"
"net/http"
"sort"
"strings"

"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)

// RewriteEntry is a rewrite array element
type RewriteEntry struct {
// Domain is the domain for which this rewrite should work.
// LegacyRewrite is a single legacy DNS rewrite record.
//
// Instances of *LegacyRewrite must never be nil.
type LegacyRewrite struct {
// Domain is the domain pattern for which this rewrite should work.
Domain string `yaml:"domain"`

// Answer is the IP address, canonical name, or one of the special
// values: "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.
// dns.TypeA or dns.TypeAAAA.
IP net.IP `yaml:"-"`

// Type is the DNS record type: A, AAAA, or CNAME.
Type uint16 `yaml:"-"`
}

// equal returns true if the entry is considered equal to the other.
func (e *RewriteEntry) equal(other RewriteEntry) (ok bool) {
return e.Domain == other.Domain && e.Answer == other.Answer
// clone returns a deep clone of rw.
func (rw *LegacyRewrite) clone() (cloneRW *LegacyRewrite) {
return &LegacyRewrite{
Domain: rw.Domain,
Answer: rw.Answer,
IP: netutil.CloneIP(rw.IP),
Type: rw.Type,
}
}

// equal returns true if the rw is equal to the other.
func (rw *LegacyRewrite) equal(other *LegacyRewrite) (ok bool) {
return rw.Domain == other.Domain && rw.Answer == other.Answer
}

// matchesQType returns true if the entry matched qtype.
func (e *RewriteEntry) matchesQType(qtype uint16) (ok bool) {
// matchesQType returns true if the entry matches the question type qt.
func (rw *LegacyRewrite) matchesQType(qt uint16) (ok bool) {
// Add CNAMEs, since they match for all types requests.
if e.Type == dns.TypeCNAME {
if rw.Type == dns.TypeCNAME {
return true
}

// Reject types other than A and AAAA.
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
if qt != dns.TypeA && qt != dns.TypeAAAA {
return false
}

// If the types match or the entry is set to allow only the other type,
// include them.
return e.Type == qtype || e.IP == nil
return rw.Type == qt || rw.IP == nil
}

// normalize makes sure that the a new or decoded entry is normalized with
// regards to domain name case, IP length, and so on.
func (e *RewriteEntry) normalize() {
// TODO(a.garipov): Write a case-agnostic version of strings.HasSuffix
// and use it in matchDomainWildcard instead of using strings.ToLower
//
// If rw is nil, it returns an errors.
func (rw *LegacyRewrite) normalize() (err error) {
if rw == nil {
return errors.Error("nil rewrite entry")
}

// TODO(a.garipov): Write a case-agnostic version of strings.HasSuffix and
// use it in matchDomainWildcard instead of using strings.ToLower
// everywhere.
e.Domain = strings.ToLower(e.Domain)
rw.Domain = strings.ToLower(rw.Domain)

switch e.Answer {
switch rw.Answer {
case "AAAA":
e.IP = nil
e.Type = dns.TypeAAAA
rw.IP = nil
rw.Type = dns.TypeAAAA

return
return nil
case "A":
e.IP = nil
e.Type = dns.TypeA
rw.IP = nil
rw.Type = dns.TypeA

return
return nil
default:
// Go on.
}

ip := net.ParseIP(e.Answer)
ip := net.ParseIP(rw.Answer)
if ip == nil {
e.Type = dns.TypeCNAME
rw.Type = dns.TypeCNAME

return
return nil
}

ip4 := ip.To4()
if ip4 != nil {
e.IP = ip4
e.Type = dns.TypeA
rw.IP = ip4
rw.Type = dns.TypeA
} else {
e.IP = ip
e.Type = dns.TypeAAAA
rw.IP = ip
rw.Type = dns.TypeAAAA
}

return nil
}

func isWildcard(host string) bool {
return len(host) > 1 && host[0] == '*' && host[1] == '.'
// isWildcard returns true if pat is a wildcard domain pattern.
func isWildcard(pat string) bool {
return len(pat) > 1 && pat[0] == '*' && pat[1] == '.'
}

// matchDomainWildcard returns true if host matches the wildcard pattern.
Expand All @@ -110,16 +134,16 @@ func matchDomainWildcard(host, wildcard string) (ok bool) {
// wildcard > exact
// lower level wildcard > higher level wildcard
//
type rewritesSorted []RewriteEntry
type rewritesSorted []*LegacyRewrite

// Len implements the sort.Interface interface for legacyRewritesSorted.
func (a rewritesSorted) Len() int { return len(a) }
func (a rewritesSorted) Len() (l int) { return len(a) }

// Swap implements the sort.Interface interface for legacyRewritesSorted.
func (a rewritesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// Less implements the sort.Interface interface for legacyRewritesSorted.
func (a rewritesSorted) Less(i, j int) bool {
func (a rewritesSorted) Less(i, j int) (less bool) {
if a[i].Type == dns.TypeCNAME && a[j].Type != dns.TypeCNAME {
return true
} else if a[i].Type != dns.TypeCNAME && a[j].Type == dns.TypeCNAME {
Expand All @@ -136,14 +160,20 @@ func (a rewritesSorted) Less(i, j int) bool {
}
}

// both are wildcards
// Both are wildcards.
return len(a[i].Domain) > len(a[j].Domain)
}

func (d *DNSFilter) prepareRewrites() {
for i := range d.Rewrites {
d.Rewrites[i].normalize()
// prepareRewrites normalizes and validates all legacy DNS rewrites.
func (d *DNSFilter) prepareRewrites() (err error) {
for i, r := range d.Rewrites {
err = r.normalize()
if err != nil {
return fmt.Errorf("at index %d: %w", i, err)
}
}

return nil
}

// findRewrites returns the list of matched rewrite entries. If rewrites are
Expand All @@ -154,10 +184,10 @@ func (d *DNSFilter) prepareRewrites() {
// host is matched exactly, wildcard entries aren't returned. If the host
// matched by wildcards, return the most specific for the question type.
func findRewrites(
entries []RewriteEntry,
entries []*LegacyRewrite,
host string,
qtype uint16,
) (rewrites []RewriteEntry, matched bool) {
) (rewrites []*LegacyRewrite, matched bool) {
for _, e := range entries {
if e.Domain != host && !matchDomainWildcard(host, e.Domain) {
continue
Expand Down Expand Up @@ -224,23 +254,32 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
}

func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
jsent := rewriteEntryJSON{}
err := json.NewDecoder(r.Body).Decode(&jsent)
rwJSON := rewriteEntryJSON{}
err := json.NewDecoder(r.Body).Decode(&rwJSON)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)

return
}

ent := RewriteEntry{
Domain: jsent.Domain,
Answer: jsent.Answer,
rw := &LegacyRewrite{
Domain: rwJSON.Domain,
Answer: rwJSON.Answer,
}

err = rw.normalize()
if err != nil {
// Shouldn't happen currently, since normalize only returns a non-nil
// error when a rewrite is nil, but be change-proof.
aghhttp.Error(r, w, http.StatusBadRequest, "normalizing: %s", err)

return
}
ent.normalize()

d.confLock.Lock()
d.Config.Rewrites = append(d.Config.Rewrites, ent)
d.Config.Rewrites = append(d.Config.Rewrites, rw)
d.confLock.Unlock()
log.Debug("rewrite: added element: %s -> %s [%d]", ent.Domain, ent.Answer, len(d.Config.Rewrites))
log.Debug("rewrite: added element: %s -> %s [%d]", rw.Domain, rw.Answer, len(d.Config.Rewrites))

d.Config.ConfigModified()
}
Expand All @@ -254,11 +293,12 @@ func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request)
return
}

entDel := RewriteEntry{
entDel := &LegacyRewrite{
Domain: jsent.Domain,
Answer: jsent.Answer,
}
arr := []RewriteEntry{}
arr := []*LegacyRewrite{}

d.confLock.Lock()
for _, ent := range d.Config.Rewrites {
if ent.equal(entDel) {
Expand Down
Loading

0 comments on commit 9213fd8

Please sign in to comment.