AdGuardHome/internal/dnsfilter/rewrites.go
Ainar Garipov 2e8352d31c Pull request #886: all: allow multiple rules in dns filter results
Merge in DNS/adguard-home from 2102-rules-result to master

Updates #2102.

Squashed commit of the following:

commit 47b2aa94c56b37be492c3c01e8111054612d9722
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 17 13:12:27 2020 +0300

    querylog: remove pre-v0.99.3 compatibility code

commit 2af0ee43c2444a7d842fcff057f2ba02f300244b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 17 13:00:27 2020 +0300

    all: improve documentation

commit 3add300a42f0aa67bb315a448e294636c85d0b3b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Dec 16 18:30:01 2020 +0300

    all: improve changelog

commit e04ef701fc2de7f4453729e617641c47e0883679
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Dec 16 17:56:53 2020 +0300

    all: improve code and documentation

commit 4f04845ae275ae4291869e00c62e4ff81b01eaa3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Dec 16 17:01:08 2020 +0300

    all: document changes, improve api

commit bc59b7656a402d0c65f13bd74a71d8dda6a8a65d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Dec 15 18:22:01 2020 +0300

    all: allow multiple rules in dns filter results
2020-12-17 13:32:46 +03:00

226 lines
4.8 KiB
Go

// DNS Rewrites
package dnsfilter
import (
"encoding/json"
"net"
"net/http"
"sort"
"strings"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// RewriteEntry is a rewrite array element
type RewriteEntry struct {
Domain string `yaml:"domain"`
Answer string `yaml:"answer"` // IP address or canonical name
Type uint16 `yaml:"-"` // DNS record type: CNAME, A or AAAA
IP net.IP `yaml:"-"` // Parsed IP address (if Type is A or AAAA)
}
func (r *RewriteEntry) equals(b RewriteEntry) bool {
return r.Domain == b.Domain && r.Answer == b.Answer
}
func isWildcard(host string) bool {
return len(host) >= 2 &&
host[0] == '*' && host[1] == '.'
}
// Return TRUE of host name matches a wildcard pattern
func matchDomainWildcard(host, wildcard string) bool {
return isWildcard(wildcard) &&
strings.HasSuffix(host, wildcard[1:])
}
type rewritesArray []RewriteEntry
func (a rewritesArray) Len() int { return len(a) }
func (a rewritesArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Priority:
// . CNAME < A/AAAA;
// . 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 {
return true
} else if a[i].Type != dns.TypeCNAME && a[j].Type == dns.TypeCNAME {
return false
}
if isWildcard(a[i].Domain) {
if !isWildcard(a[j].Domain) {
return false
}
} else {
if isWildcard(a[j].Domain) {
return true
}
}
// both are wildcards
return len(a[i].Domain) > len(a[j].Domain)
}
// Prepare entry for use
func (r *RewriteEntry) prepare() {
if r.Answer == "AAAA" {
r.IP = nil
r.Type = dns.TypeAAAA
return
} else if r.Answer == "A" {
r.IP = nil
r.Type = dns.TypeA
return
}
ip := net.ParseIP(r.Answer)
if ip == nil {
r.Type = dns.TypeCNAME
return
}
r.IP = ip
r.Type = dns.TypeAAAA
ip4 := ip.To4()
if ip4 != nil {
r.IP = ip4
r.Type = dns.TypeA
}
}
func (d *DNSFilter) prepareRewrites() {
for i := range d.Rewrites {
d.Rewrites[i].prepare()
}
}
// Get the list of matched rewrite entries.
// Priority: CNAME, A/AAAA; exact, wildcard.
// If matched exactly, don't return wildcard entries.
// If matched by several wildcards, select the more specific one
func findRewrites(a []RewriteEntry, host string) []RewriteEntry {
rr := rewritesArray{}
for _, r := range a {
if r.Domain != host {
if !matchDomainWildcard(host, r.Domain) {
continue
}
}
rr = append(rr, r)
}
if len(rr) == 0 {
return nil
}
sort.Sort(rr)
isWC := isWildcard(rr[0].Domain)
if !isWC {
for i, r := range rr {
if isWildcard(r.Domain) {
rr = rr[:i]
break
}
}
} else {
rr = rr[:1]
}
return rr
}
func rewriteArrayDup(a []RewriteEntry) []RewriteEntry {
a2 := make([]RewriteEntry, len(a))
copy(a2, a)
return a2
}
type rewriteEntryJSON struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
}
func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
arr := []*rewriteEntryJSON{}
d.confLock.Lock()
for _, ent := range d.Config.Rewrites {
jsent := rewriteEntryJSON{
Domain: ent.Domain,
Answer: ent.Answer,
}
arr = append(arr, &jsent)
}
d.confLock.Unlock()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(arr)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "json.Encode: %s", err)
return
}
}
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
jsent := rewriteEntryJSON{}
err := json.NewDecoder(r.Body).Decode(&jsent)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
ent := RewriteEntry{
Domain: jsent.Domain,
Answer: jsent.Answer,
}
ent.prepare()
d.confLock.Lock()
d.Config.Rewrites = append(d.Config.Rewrites, ent)
d.confLock.Unlock()
log.Debug("Rewrites: added element: %s -> %s [%d]",
ent.Domain, ent.Answer, len(d.Config.Rewrites))
d.Config.ConfigModified()
}
func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
jsent := rewriteEntryJSON{}
err := json.NewDecoder(r.Body).Decode(&jsent)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
entDel := RewriteEntry{
Domain: jsent.Domain,
Answer: jsent.Answer,
}
arr := []RewriteEntry{}
d.confLock.Lock()
for _, ent := range d.Config.Rewrites {
if ent.equals(entDel) {
log.Debug("Rewrites: removed element: %s -> %s", ent.Domain, ent.Answer)
continue
}
arr = append(arr, ent)
}
d.Config.Rewrites = arr
d.confLock.Unlock()
d.Config.ConfigModified()
}
func (d *DNSFilter) registerRewritesHandlers() {
d.Config.HTTPRegister("GET", "/control/rewrite/list", d.handleRewriteList)
d.Config.HTTPRegister("POST", "/control/rewrite/add", d.handleRewriteAdd)
d.Config.HTTPRegister("POST", "/control/rewrite/delete", d.handleRewriteDelete)
}