Pull request: 3846 hosts querylog

Merge in DNS/adguard-home from 3846-hosts-querylog to master

Updates #3846.

Squashed commit of the following:

commit 722e96628b1ccca1a5b5a716b8bcb1da2aefcc3b
Merge: a20ad71e ed868fa4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Nov 23 17:52:08 2021 +0300

    Merge branch 'master' into 3846-hosts-querylog

commit a20ad71e723dbfa3483c3bdf9e4c8fd15c8b0e3c
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Nov 23 17:28:12 2021 +0300

    client: fix variable name

commit 7013bff05d6cff75c6c25a38d614db8b4b2f0b87
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Nov 23 17:03:26 2021 +0300

    client: fix missing import

commit 8e4a0fb047b4d39ab44a285f59420573d7ba5eec
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Nov 23 16:56:50 2021 +0300

    client: handle system host filter id

commit abbbf662d2f3ea3f5d3569a9c45418e356adbf3c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Nov 22 13:54:52 2021 +0300

    all: imp code

commit c2df63e46e75f84f70a610d18deccbeee672ebda
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Nov 22 12:50:51 2021 +0300

    querylog: rm unused test data

commit 8a1d47d266254fd4aedd4c61c7ea9e48168ea375
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Nov 22 02:52:50 2021 +0300

    aghnet: final imps

commit ade3acb4bebc8bdd755e56f314cdf19bc9375557
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 19 15:48:40 2021 +0300

    all: add hosts container rule list support
This commit is contained in:
Eugene Burkov 2021-11-23 18:01:48 +03:00
parent ed868fa46a
commit 51f11d2f8e
11 changed files with 475 additions and 287 deletions

View File

@ -201,6 +201,7 @@
"form_error_url_or_path_format": "Invalid URL or absolute path of the list", "form_error_url_or_path_format": "Invalid URL or absolute path of the list",
"custom_filter_rules": "Custom filtering rules", "custom_filter_rules": "Custom filtering rules",
"custom_filter_rules_hint": "Enter one rule on a line. You can use either adblock rules or hosts files syntax.", "custom_filter_rules_hint": "Enter one rule on a line. You can use either adblock rules or hosts files syntax.",
"system_host_files": "System hosts files",
"examples_title": "Examples", "examples_title": "Examples",
"example_meaning_filter_block": "block access to the example.org domain and all its subdomains", "example_meaning_filter_block": "block access to the example.org domain and all its subdomains",
"example_meaning_filter_whitelist": "unblock access to the example.org domain and all its subdomains", "example_meaning_filter_whitelist": "unblock access to the example.org domain and all its subdomains",

View File

@ -529,6 +529,7 @@ export const DETAILED_DATE_FORMAT_OPTIONS = {
}; };
export const CUSTOM_FILTERING_RULES_ID = 0; export const CUSTOM_FILTERING_RULES_ID = 0;
export const SYSTEM_HOSTS_FILTER_ID = -1;
export const BLOCK_ACTIONS = { export const BLOCK_ACTIONS = {
BLOCK: 'block', BLOCK: 'block',

View File

@ -26,6 +26,7 @@ import {
STANDARD_DNS_PORT, STANDARD_DNS_PORT,
STANDARD_HTTPS_PORT, STANDARD_HTTPS_PORT,
STANDARD_WEB_PORT, STANDARD_WEB_PORT,
SYSTEM_HOSTS_FILTER_ID,
} from './constants'; } from './constants';
/** /**
@ -791,9 +792,12 @@ export const getFilterName = (
return i18n.t(customFilterTranslationKey); return i18n.t(customFilterTranslationKey);
} }
if (filterId === SYSTEM_HOSTS_FILTER_ID) {
return i18n.t('system_host_files');
}
const matchIdPredicate = (filter) => filter.id === filterId; const matchIdPredicate = (filter) => filter.id === filterId;
const filter = filters.find(matchIdPredicate) || whitelistFilters.find(matchIdPredicate); const filter = filters.find(matchIdPredicate) || whitelistFilters.find(matchIdPredicate);
return resolveFilterName(filter); return resolveFilterName(filter);
}; };

View File

@ -35,7 +35,7 @@ func (lr *limitedReader) Read(p []byte) (n int, err error) {
} }
if int64(len(p)) > lr.n { if int64(len(p)) > lr.n {
p = p[0:lr.n] p = p[:lr.n]
} }
n, err = lr.r.Read(p) n, err = lr.r.Read(p)

View File

@ -28,6 +28,69 @@ func DefaultHostsPaths() (paths []string) {
return defaultHostsPaths() return defaultHostsPaths()
} }
// requestMatcher combines the logic for matching requests and translating the
// appropriate rules.
type requestMatcher struct {
// stateLock protects all the fields of requestMatcher.
stateLock *sync.RWMutex
// rulesStrg stores the rules obtained from the hosts' file.
rulesStrg *filterlist.RuleStorage
// engine serves rulesStrg.
engine *urlfilter.DNSEngine
// translator maps generated $dnsrewrite rules into hosts-syntax rules.
//
// TODO(e.burkov): Store the filename from which the rule was parsed.
translator map[string]string
}
// MatchRequest processes the request rewriting hostnames and addresses read
// from the operating system's hosts files.
//
// res is nil for any request having not an A/AAAA or PTR type. Results
// containing CNAME information may be queried again with the same question type
// and the returned CNAME for Host field of request. Results are guaranteed to
// be direct, i.e. any returned CNAME resolves into actual address like an alias
// in hosts does, see man hosts (5).
//
// It's safe for concurrent use.
func (rm *requestMatcher) MatchRequest(
req urlfilter.DNSRequest,
) (res *urlfilter.DNSResult, ok bool) {
switch req.DNSType {
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
log.Debug("%s: handling the request", hostsContainerPref)
default:
return nil, false
}
rm.stateLock.RLock()
defer rm.stateLock.RUnlock()
return rm.engine.MatchRequest(req)
}
// Translate returns the source hosts-syntax rule for the generated dnsrewrite
// rule or an empty string if the last doesn't exist.
func (rm *requestMatcher) Translate(rule string) (hostRule string) {
rm.stateLock.RLock()
defer rm.stateLock.RUnlock()
return rm.translator[rule]
}
// resetEng updates container's engine and the translation map.
func (rm *requestMatcher) resetEng(rulesStrg *filterlist.RuleStorage, tr map[string]string) {
rm.stateLock.Lock()
defer rm.stateLock.Unlock()
rm.rulesStrg = rulesStrg
rm.engine = urlfilter.NewDNSEngine(rm.rulesStrg)
rm.translator = tr
}
// hostsContainerPref is a prefix for logging and wrapping errors in // hostsContainerPref is a prefix for logging and wrapping errors in
// HostsContainer's methods. // HostsContainer's methods.
const hostsContainerPref = "hosts container" const hostsContainerPref = "hosts container"
@ -35,13 +98,9 @@ const hostsContainerPref = "hosts container"
// HostsContainer stores the relevant hosts database provided by the OS and // HostsContainer stores the relevant hosts database provided by the OS and
// processes both A/AAAA and PTR DNS requests for those. // processes both A/AAAA and PTR DNS requests for those.
type HostsContainer struct { type HostsContainer struct {
// engLock protects rulesStrg and engine. // requestMatcher matches the requests and translates the rules. It's
engLock *sync.RWMutex // embedded to implement MatchRequest and Translate for *HostsContainer.
requestMatcher
// rulesStrg stores the rules obtained from the hosts' file.
rulesStrg *filterlist.RuleStorage
// engine serves rulesStrg.
engine *urlfilter.DNSEngine
// done is the channel to sign closing the container. // done is the channel to sign closing the container.
done chan struct{} done chan struct{}
@ -87,7 +146,9 @@ func NewHostsContainer(
} }
hc = &HostsContainer{ hc = &HostsContainer{
engLock: &sync.RWMutex{}, requestMatcher: requestMatcher{
stateLock: &sync.RWMutex{},
},
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
updates: make(chan *netutil.IPMap, 1), updates: make(chan *netutil.IPMap, 1),
fsys: fsys, fsys: fsys,
@ -117,25 +178,6 @@ func NewHostsContainer(
return hc, nil return hc, nil
} }
// MatchRequest is the request processing method to resolve hostnames and
// addresses from the operating system's hosts files. res is nil for any
// request having not an A/AAAA or PTR type. It's safe for concurrent use.
func (hc *HostsContainer) MatchRequest(
req urlfilter.DNSRequest,
) (res *urlfilter.DNSResult, ok bool) {
switch req.DNSType {
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
log.Debug("%s: handling the request", hostsContainerPref)
default:
return nil, false
}
hc.engLock.RLock()
defer hc.engLock.RUnlock()
return hc.engine.MatchRequest(req)
}
// Close implements the io.Closer interface for *HostsContainer. Close must // Close implements the io.Closer interface for *HostsContainer. Close must
// only be called once. The returned err is always nil. // only be called once. The returned err is always nil.
func (hc *HostsContainer) Close() (err error) { func (hc *HostsContainer) Close() (err error) {
@ -203,10 +245,17 @@ func (hc *HostsContainer) handleEvents() {
} }
// hostsParser is a helper type to parse rules from the operating system's hosts // hostsParser is a helper type to parse rules from the operating system's hosts
// file. // file. It exists for only a single refreshing session.
type hostsParser struct { type hostsParser struct {
// rules builds the resulting rules list content. // rulesBuilder builds the resulting rulesBuilder list content.
rules *strings.Builder rulesBuilder *strings.Builder
// translations maps generated $dnsrewrite rules to the hosts-translations
// rules.
translations map[string]string
// cnameSet prevents duplicating cname rules.
cnameSet *stringutil.Set
// table stores only the unique IP-hostname pairs. It's also sent to the // table stores only the unique IP-hostname pairs. It's also sent to the
// updates channel afterwards. // updates channel afterwards.
@ -215,7 +264,10 @@ type hostsParser struct {
func (hc *HostsContainer) newHostsParser() (hp *hostsParser) { func (hc *HostsContainer) newHostsParser() (hp *hostsParser) {
return &hostsParser{ return &hostsParser{
rules: &strings.Builder{}, rulesBuilder: &strings.Builder{},
// For A/AAAA and PTRs.
translations: make(map[string]string, hc.last.Len()*2),
cnameSet: stringutil.NewSet(),
table: netutil.NewIPMap(hc.last.Len()), table: netutil.NewIPMap(hc.last.Len()),
} }
} }
@ -234,9 +286,7 @@ func (hp *hostsParser) parseFile(
continue continue
} }
for _, host := range hosts { hp.addPairs(ip, hosts)
hp.addPair(ip, host)
}
} }
return nil, true, s.Err() return nil, true, s.Err()
@ -244,7 +294,6 @@ func (hp *hostsParser) parseFile(
// parseLine parses the line having the hosts syntax ignoring invalid ones. // parseLine parses the line having the hosts syntax ignoring invalid ones.
func (hp *hostsParser) parseLine(line string) (ip net.IP, hosts []string) { func (hp *hostsParser) parseLine(line string) (ip net.IP, hosts []string) {
line = strings.TrimSpace(line)
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) < 2 { if len(fields) < 2 {
return nil, nil return nil, nil
@ -274,74 +323,142 @@ loop:
return ip, hosts return ip, hosts
} }
// add returns true if the pair of ip and host wasn't added to the hp before. // Simple types of hosts in hosts database. Zero value isn't used to be able
func (hp *hostsParser) add(ip net.IP, host string) (added bool) { // quizzaciously emulate nil with 0.
const (
_ = iota
hostAlias
hostMain
)
// add tries to add the ip-host pair. It returns:
//
// hostAlias if the host is not the first one added for the ip.
// hostMain if the host is the first one added for the ip.
// 0 if the ip-host pair has already been added.
//
func (hp *hostsParser) add(ip net.IP, host string) (hostType int) {
v, ok := hp.table.Get(ip) v, ok := hp.table.Get(ip)
hosts, _ := v.(*stringutil.Set) switch hosts, _ := v.(*stringutil.Set); {
switch {
case ok && hosts.Has(host): case ok && hosts.Has(host):
return false return 0
case hosts == nil: case hosts == nil:
hosts = stringutil.NewSet(host) hosts = stringutil.NewSet(host)
hp.table.Set(ip, hosts) hp.table.Set(ip, hosts)
return hostMain
default: default:
hosts.Add(host) hosts.Add(host)
return hostAlias
}
} }
return true // addPair puts the pair of ip and host to the rules builder if needed. For
// each ip the first member of hosts will become the main one.
func (hp *hostsParser) addPairs(ip net.IP, hosts []string) {
// Put the rule in a preproccesed format like:
//
// ip host1 host2 ...
//
hostsLine := strings.Join(append([]string{ip.String()}, hosts...), " ")
var mainHost string
for _, host := range hosts {
switch hp.add(ip, host) {
case 0:
continue
case hostMain:
mainHost = host
added, addedPtr := hp.writeMainHostRule(host, ip)
hp.translations[added], hp.translations[addedPtr] = hostsLine, hostsLine
case hostAlias:
pair := fmt.Sprint(host, " ", mainHost)
if hp.cnameSet.Has(pair) {
continue
}
// Since the hostAlias couldn't be returned from add before the
// hostMain the mainHost shouldn't appear empty.
hp.writeAliasHostRule(host, mainHost)
hp.cnameSet.Add(pair)
} }
// addPair puts the pair of ip and host to the rules builder if needed. log.Debug("%s: added ip-host pair %q-%q", hostsContainerPref, ip, host)
func (hp *hostsParser) addPair(ip net.IP, host string) { }
}
// writeAliasHostRule writes the CNAME rule for the alias-host pair into
// internal builders.
func (hp *hostsParser) writeAliasHostRule(alias, host string) {
const (
nl = "\n"
sc = ";"
rwSuccess = rules.MaskSeparator + "$dnsrewrite=NOERROR" + sc + "CNAME" + sc
constLen = len(rules.MaskStartURL) + len(rwSuccess) + len(nl)
)
hp.rulesBuilder.Grow(constLen + len(host) + len(alias))
stringutil.WriteToBuilder(hp.rulesBuilder, rules.MaskStartURL, alias, rwSuccess, host, nl)
}
// writeMainHostRule writes the actual rule for the qtype and the PTR for the
// host-ip pair into internal builders.
func (hp *hostsParser) writeMainHostRule(host string, ip net.IP) (added, addedPtr string) {
arpa, err := netutil.IPToReversedAddr(ip) arpa, err := netutil.IPToReversedAddr(ip)
if err != nil { if err != nil {
return return
} }
if !hp.add(ip, host) {
return
}
qtype := "AAAA"
if ip.To4() != nil {
// Assume the validation of the IP address is performed already.
qtype = "A"
}
const ( const (
nl = "\n" nl = "\n"
sc = ";"
rewriteSuccess = "$dnsrewrite=NOERROR" + sc rwSuccess = "^$dnsrewrite=NOERROR;"
rewriteSuccessPTR = rewriteSuccess + "PTR" + sc rwSuccessPTR = "^$dnsrewrite=NOERROR;PTR;"
modLen = len("||") + len(rwSuccess)
modLenPTR = len("||") + len(rwSuccessPTR)
) )
var qtype string
// The validation of the IP address has been performed earlier so it is
// guaranteed to be either an IPv4 or an IPv6.
if ip.To4() != nil {
qtype = "A"
} else {
qtype = "AAAA"
}
ipStr := ip.String() ipStr := ip.String()
fqdn := dns.Fqdn(host) fqdn := dns.Fqdn(host)
for _, ruleData := range [...][]string{{ ruleBuilder := &strings.Builder{}
// A/AAAA. ruleBuilder.Grow(modLen + len(host) + len(qtype) + len(ipStr))
rules.MaskStartURL, stringutil.WriteToBuilder(
ruleBuilder,
"||",
host, host,
rules.MaskSeparator, rwSuccess,
rewriteSuccess,
qtype, qtype,
sc, ";",
ipStr, ipStr,
nl, )
}, { added = ruleBuilder.String()
// PTR.
rules.MaskStartURL,
arpa,
rules.MaskSeparator,
rewriteSuccessPTR,
fqdn,
nl,
}} {
stringutil.WriteToBuilder(hp.rules, ruleData...)
}
log.Debug("%s: added ip-host pair %q/%q", hostsContainerPref, ip, host) ruleBuilder.Reset()
ruleBuilder.Grow(modLenPTR + len(arpa) + len(fqdn))
stringutil.WriteToBuilder(
ruleBuilder,
"||",
arpa,
rwSuccessPTR,
fqdn,
)
addedPtr = ruleBuilder.String()
hp.rulesBuilder.Grow(len(added) + len(addedPtr) + 2*len(nl))
stringutil.WriteToBuilder(hp.rulesBuilder, added, nl, addedPtr, nl)
return added, addedPtr
} }
// equalSet returns true if the internal hosts table just parsed equals target. // equalSet returns true if the internal hosts table just parsed equals target.
@ -385,15 +502,16 @@ func (hp *hostsParser) sendUpd(ch chan *netutil.IPMap) {
case ch <- upd: case ch <- upd:
// The previous update was just read and the next one pushed. Go on. // The previous update was just read and the next one pushed. Go on.
default: default:
log.Debug("%s: the channel is broken", hostsContainerPref) log.Error("%s: the updates channel is broken", hostsContainerPref)
} }
} }
// newStrg creates a new rules storage from parsed data. // newStrg creates a new rules storage from parsed data.
func (hp *hostsParser) newStrg() (s *filterlist.RuleStorage, err error) { func (hp *hostsParser) newStrg() (s *filterlist.RuleStorage, err error) {
return filterlist.NewRuleStorage([]filterlist.RuleList{&filterlist.StringRuleList{ return filterlist.NewRuleStorage([]filterlist.RuleList{&filterlist.StringRuleList{
// TODO(e.burkov): Make configurable.
ID: -1, ID: -1,
RulesText: hp.rules.String(), RulesText: hp.rulesBuilder.String(),
IgnoreCosmetic: true, IgnoreCosmetic: true,
}}) }})
} }
@ -424,15 +542,7 @@ func (hc *HostsContainer) refresh() (err error) {
return fmt.Errorf("initializing rules storage: %w", err) return fmt.Errorf("initializing rules storage: %w", err)
} }
hc.resetEng(rulesStrg) hc.resetEng(rulesStrg, hp.translations)
return nil return nil
} }
func (hc *HostsContainer) resetEng(rulesStrg *filterlist.RuleStorage) {
hc.engLock.Lock()
defer hc.engLock.Unlock()
hc.rulesStrg = rulesStrg
hc.engine = urlfilter.NewDNSEngine(hc.rulesStrg)
}

View File

@ -3,6 +3,7 @@ package aghnet
import ( import (
"io/fs" "io/fs"
"net" "net"
"os"
"path" "path"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -11,9 +12,9 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter" "github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -203,129 +204,6 @@ func TestHostsContainer_Refresh(t *testing.T) {
}) })
} }
func TestHostsContainer_MatchRequest(t *testing.T) {
var (
ip4 = net.IP{127, 0, 0, 1}
ip6 = net.IP{
128, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 1,
}
hostname4 = "localhost"
hostname6 = "localhostv6"
hostname4a = "abcd"
reversed4, _ = netutil.IPToReversedAddr(ip4)
reversed6, _ = netutil.IPToReversedAddr(ip6)
)
const filename = "file1"
gsfs := fstest.MapFS{
filename: &fstest.MapFile{Data: []byte(
ip4.String() + " " + hostname4 + " " + hostname4a + nl +
ip6.String() + " " + hostname6 + nl +
`256.256.256.256 fakebroadcast` + nl,
)},
}
hc, err := NewHostsContainer(gsfs, &aghtest.FSWatcher{
OnEvents: func() (e <-chan struct{}) { panic("not implemented") },
OnAdd: func(name string) (err error) {
assert.Equal(t, filename, name)
return nil
},
OnClose: func() (err error) { panic("not implemented") },
}, filename)
require.NoError(t, err)
testCase := []struct {
name string
want []interface{}
req urlfilter.DNSRequest
}{{
name: "a",
want: []interface{}{ip4.To16()},
req: urlfilter.DNSRequest{
Hostname: hostname4,
DNSType: dns.TypeA,
},
}, {
name: "a_for_aaaa",
want: []interface{}{
ip4.To16(),
},
req: urlfilter.DNSRequest{
Hostname: hostname4,
DNSType: dns.TypeAAAA,
},
}, {
name: "aaaa",
want: []interface{}{ip6},
req: urlfilter.DNSRequest{
Hostname: hostname6,
DNSType: dns.TypeAAAA,
},
}, {
name: "ptr",
want: []interface{}{
dns.Fqdn(hostname4),
dns.Fqdn(hostname4a),
},
req: urlfilter.DNSRequest{
Hostname: reversed4,
DNSType: dns.TypePTR,
},
}, {
name: "ptr_v6",
want: []interface{}{dns.Fqdn(hostname6)},
req: urlfilter.DNSRequest{
Hostname: reversed6,
DNSType: dns.TypePTR,
},
}, {
name: "a_alias",
want: []interface{}{ip4.To16()},
req: urlfilter.DNSRequest{
Hostname: hostname4a,
DNSType: dns.TypeA,
},
}}
for _, tc := range testCase {
t.Run(tc.name, func(t *testing.T) {
res, ok := hc.MatchRequest(tc.req)
require.False(t, ok)
require.NotNil(t, res)
rws := res.DNSRewrites()
require.Len(t, rws, len(tc.want))
for i, w := range tc.want {
require.NotNil(t, rws[i])
rw := rws[i].DNSRewrite
require.NotNil(t, rw)
assert.Equal(t, w, rw.Value)
}
})
}
t.Run("cname", func(t *testing.T) {
res, ok := hc.MatchRequest(urlfilter.DNSRequest{
Hostname: hostname4,
DNSType: dns.TypeCNAME,
})
require.False(t, ok)
assert.Nil(t, res)
})
}
func TestHostsContainer_PathsToPatterns(t *testing.T) { func TestHostsContainer_PathsToPatterns(t *testing.T) {
const ( const (
dir0 = "dir" dir0 = "dir"
@ -412,53 +290,108 @@ func TestHostsContainer_PathsToPatterns(t *testing.T) {
}) })
} }
func TestUniqueRules_AddPair(t *testing.T) { func TestHostsContainer(t *testing.T) {
knownIP := net.IP{1, 2, 3, 4} testdata := os.DirFS("./testdata")
const knownHost = "host1" nRewrites := func(t *testing.T, res *urlfilter.DNSResult, n int) (rws []*rules.DNSRewrite) {
t.Helper()
ipToHost := netutil.NewIPMap(0) rewrites := res.DNSRewrites()
ipToHost.Set(knownIP, *stringutil.NewSet(knownHost)) assert.Len(t, rewrites, n)
testCases := []struct { for _, rewrite := range rewrites {
name string rw := rewrite.DNSRewrite
host string require.NotNil(t, rw)
wantRules string
ip net.IP
}{{
name: "new_one",
host: "host2",
wantRules: "||host2^$dnsrewrite=NOERROR;A;1.2.3.4\n" +
"||4.3.2.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;host2.\n",
ip: knownIP,
}, {
name: "existing_one",
host: knownHost,
wantRules: "||" + knownHost + "^$dnsrewrite=NOERROR;A;1.2.3.4\n" +
"||4.3.2.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;host1.\n",
ip: knownIP,
}, {
name: "new_ip",
host: knownHost,
wantRules: "||" + knownHost + "^$dnsrewrite=NOERROR;A;1.2.3.5\n" +
"||5.3.2.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;" + knownHost + ".\n",
ip: net.IP{1, 2, 3, 5},
}, {
name: "bad_ip",
host: knownHost,
wantRules: "",
ip: net.IP{1, 2, 3, 4, 5},
}}
for _, tc := range testCases { rws = append(rws, rw)
hp := hostsParser{
rules: &strings.Builder{},
table: ipToHost.ShallowClone(),
} }
return rws
}
testCases := []struct {
testTail func(t *testing.T, res *urlfilter.DNSResult)
name string
req urlfilter.DNSRequest
}{{
name: "simple",
req: urlfilter.DNSRequest{
Hostname: "simplehost",
DNSType: dns.TypeA,
},
testTail: func(t *testing.T, res *urlfilter.DNSResult) {
rws := nRewrites(t, res, 2)
v, ok := rws[0].Value.(net.IP)
require.True(t, ok)
assert.True(t, net.IP{1, 0, 0, 1}.Equal(v))
v, ok = rws[1].Value.(net.IP)
require.True(t, ok)
// It's ::1.
assert.True(t, net.IP(append((&[15]byte{})[:], byte(1))).Equal(v))
},
}, {
name: "hello_alias",
req: urlfilter.DNSRequest{
Hostname: "hello.world",
DNSType: dns.TypeA,
},
testTail: func(t *testing.T, res *urlfilter.DNSResult) {
assert.Equal(t, "hello", nRewrites(t, res, 1)[0].NewCNAME)
},
}, {
name: "lots_of_aliases",
req: urlfilter.DNSRequest{
Hostname: "for.testing",
DNSType: dns.TypeA,
},
testTail: func(t *testing.T, res *urlfilter.DNSResult) {
assert.Equal(t, "a.whole", nRewrites(t, res, 1)[0].NewCNAME)
},
}, {
name: "reverse",
req: urlfilter.DNSRequest{
Hostname: "1.0.0.1.in-addr.arpa",
DNSType: dns.TypePTR,
},
testTail: func(t *testing.T, res *urlfilter.DNSResult) {
rws := nRewrites(t, res, 1)
assert.Equal(t, dns.TypePTR, rws[0].RRType)
assert.Equal(t, "simplehost.", rws[0].Value)
},
}, {
name: "non-existing",
req: urlfilter.DNSRequest{
Hostname: "nonexisting",
DNSType: dns.TypeA,
},
testTail: func(t *testing.T, res *urlfilter.DNSResult) {
require.NotNil(t, res)
assert.Nil(t, res.DNSRewrites())
},
}}
stubWatcher := aghtest.FSWatcher{
OnEvents: func() (e <-chan struct{}) { return nil },
OnAdd: func(name string) (err error) { return nil },
OnClose: func() (err error) { panic("not implemented") },
}
hc, err := NewHostsContainer(testdata, &stubWatcher, "etc_hosts")
require.NoError(t, err)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
hp.addPair(tc.ip, tc.host) res, ok := hc.MatchRequest(tc.req)
assert.Equal(t, tc.wantRules, hp.rules.String()) require.False(t, ok)
require.NotNil(t, res)
tc.testTail(t, res)
}) })
} }
} }

14
internal/aghnet/testdata/etc_hosts vendored Normal file
View File

@ -0,0 +1,14 @@
#
# Test /etc/hosts file
#
1.0.0.1 simplehost
1.0.0.0 hello hello.world
# See https://github.com/AdguardTeam/AdGuardHome/issues/3846.
1.0.0.2 a.whole lot.of aliases for.testing
# Same for IPv6.
::1 simplehost
:: hello hello.world
::2 a.whole lot.of aliases for.testing

View File

@ -27,7 +27,7 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
for _, nr := range dnsr { for _, nr := range dnsr {
dr := nr.DNSRewrite dr := nr.DNSRewrite
if dr.NewCNAME != "" { if dr.NewCNAME != "" {
// NewCNAME rules have a higher priority than the other rules. // NewCNAME rules have a higher priority than other rules.
rules = []*ResultRule{{ rules = []*ResultRule{{
FilterListID: int64(nr.GetFilterListID()), FilterListID: int64(nr.GetFilterListID()),
Text: nr.RuleText, Text: nr.RuleText,

View File

@ -376,16 +376,8 @@ type Result struct {
// Rules are applied rules. If Rules are not empty, each rule is not nil. // Rules are applied rules. If Rules are not empty, each rule is not nil.
Rules []*ResultRule `json:",omitempty"` Rules []*ResultRule `json:",omitempty"`
// ReverseHosts is the reverse lookup rewrite result. It is empty unless
// Reason is set to RewrittenAutoHosts.
//
// TODO(e.burkov): There is no need for AutoHosts-related fields any more
// since the hosts container now uses $dnsrewrite rules. These fields are
// only used in query log to decode old format.
ReverseHosts []string `json:",omitempty"`
// IPList is the lookup rewrite result. It is empty unless Reason is set to // IPList is the lookup rewrite result. It is empty unless Reason is set to
// RewrittenAutoHosts or Rewritten. // Rewritten.
IPList []net.IP `json:",omitempty"` IPList []net.IP `json:",omitempty"`
// CanonName is the CNAME value from the lookup rewrite result. It is empty // CanonName is the CNAME value from the lookup rewrite result. It is empty
@ -464,7 +456,7 @@ func (d *DNSFilter) matchSysHosts(
return res, nil return res, nil
} }
dnsres, _ := d.EtcHosts.MatchRequest(urlfilter.DNSRequest{ return d.matchSysHostsIntl(&urlfilter.DNSRequest{
Hostname: host, Hostname: host,
SortedClientTags: setts.ClientTags, SortedClientTags: setts.ClientTags,
// TODO(e.burkov): Wait for urlfilter update to pass net.IP. // TODO(e.burkov): Wait for urlfilter update to pass net.IP.
@ -472,18 +464,34 @@ func (d *DNSFilter) matchSysHosts(
ClientName: setts.ClientName, ClientName: setts.ClientName,
DNSType: qtype, DNSType: qtype,
}) })
}
// matchSysHostsIntl actually matches the request. It's separated to avoid
// perfoming checks twice.
func (d *DNSFilter) matchSysHostsIntl(
req *urlfilter.DNSRequest,
) (res Result, err error) {
dnsres, _ := d.EtcHosts.MatchRequest(*req)
if dnsres == nil { if dnsres == nil {
return res, nil return res, nil
} }
if dnsr := dnsres.DNSRewrites(); len(dnsr) > 0 { dnsr := dnsres.DNSRewrites()
// Check DNS rewrites first, because the API there is a bit awkward. if len(dnsr) == 0 {
return res, nil
}
res = d.processDNSRewrites(dnsr) res = d.processDNSRewrites(dnsr)
if cn := res.CanonName; cn != "" {
// Probably an alias.
req.Hostname = cn
return d.matchSysHostsIntl(req)
}
res.Reason = RewrittenAutoHosts res.Reason = RewrittenAutoHosts
// TODO(e.burkov): Put real hosts-syntax rules. for _, r := range res.Rules {
// r.Text = stringutil.Coalesce(d.EtcHosts.Translate(r.Text), r.Text)
// See https://github.com/AdguardTeam/AdGuardHome/issues/3846.
res.Rules = nil
} }
return res, nil return res, nil
@ -799,7 +807,6 @@ func (d *DNSFilter) matchHost(
} }
dnsres, ok := d.filteringEngine.MatchRequest(ureq) dnsres, ok := d.filteringEngine.MatchRequest(ureq)
// Check DNS rewrites first, because the API there is a bit awkward. // Check DNS rewrites first, because the API there is a bit awkward.
if dnsr := dnsres.DNSRewrites(); len(dnsr) > 0 { if dnsr := dnsres.DNSRewrites(); len(dnsr) > 0 {
res = d.processDNSRewrites(dnsr) res = d.processDNSRewrites(dnsr)

View File

@ -291,10 +291,13 @@ func decodeResultRules(dec *json.Decoder, ent *logEntry) {
} }
if d, ok := keyToken.(json.Delim); ok { if d, ok := keyToken.(json.Delim); ok {
if d == '}' { switch d {
case '}':
i++ i++
} else if d == ']' { case ']':
return return
default:
// Go on.
} }
continue continue
@ -312,6 +315,11 @@ func decodeResultRules(dec *json.Decoder, ent *logEntry) {
} }
} }
// decodeResultReverseHosts parses the dec's tokens into ent interpreting it as
// the result of hosts container's $dnsrewrite rule. It assumes there are no
// other occurrences of DNSRewriteResult in the entry since hosts container's
// rewrites currently has the highest priority along the entire filtering
// pipeline.
func decodeResultReverseHosts(dec *json.Decoder, ent *logEntry) { func decodeResultReverseHosts(dec *json.Decoder, ent *logEntry) {
for { for {
itemToken, err := dec.Token() itemToken, err := dec.Token()
@ -335,7 +343,25 @@ func decodeResultReverseHosts(dec *json.Decoder, ent *logEntry) {
return return
case string: case string:
ent.Result.ReverseHosts = append(ent.Result.ReverseHosts, v) v = dns.Fqdn(v)
if res := &ent.Result; res.DNSRewriteResult == nil {
res.DNSRewriteResult = &filtering.DNSRewriteResult{
RCode: dns.RcodeSuccess,
Response: filtering.DNSRewriteResultResponse{
dns.TypePTR: []rules.RRValue{v},
},
}
continue
} else {
res.DNSRewriteResult.RCode = dns.RcodeSuccess
}
if rres := ent.Result.DNSRewriteResult; rres.Response == nil {
rres.Response = filtering.DNSRewriteResultResponse{dns.TypePTR: []rules.RRValue{v}}
} else {
rres.Response[dns.TypePTR] = append(rres.Response[dns.TypePTR], v)
}
default: default:
continue continue
} }
@ -407,9 +433,9 @@ func decodeResultDNSRewriteResultKey(key string, dec *json.Decoder, ent *logEntr
ent.Result.DNSRewriteResult.Response = filtering.DNSRewriteResultResponse{} ent.Result.DNSRewriteResult.Response = filtering.DNSRewriteResultResponse{}
} }
// TODO(a.garipov): I give up. This whole file is a mess. // TODO(a.garipov): I give up. This whole file is a mess. Luckily, we
// Luckily, we can assume that this field is relatively rare and // can assume that this field is relatively rare and just use the normal
// just use the normal decoding and correct the values. // decoding and correct the values.
err = dec.Decode(&ent.Result.DNSRewriteResult.Response) err = dec.Decode(&ent.Result.DNSRewriteResult.Response)
if err != nil { if err != nil {
log.Debug("decodeResultDNSRewriteResultKey response err: %s", err) log.Debug("decodeResultDNSRewriteResultKey response err: %s", err)
@ -463,7 +489,40 @@ func decodeResultDNSRewriteResult(dec *json.Decoder, ent *logEntry) {
} }
} }
// translateResult converts some fields of the ent.Result to the format
// consistent with current implementation.
func translateResult(ent *logEntry) {
res := &ent.Result
if res.Reason != filtering.RewrittenAutoHosts || len(res.IPList) == 0 {
return
}
if res.DNSRewriteResult == nil {
res.DNSRewriteResult = &filtering.DNSRewriteResult{
RCode: dns.RcodeSuccess,
}
}
if res.DNSRewriteResult.Response == nil {
res.DNSRewriteResult.Response = filtering.DNSRewriteResultResponse{}
}
resp := res.DNSRewriteResult.Response
for _, ip := range res.IPList {
qType := dns.TypeAAAA
if ip.To4() != nil {
qType = dns.TypeA
}
resp[qType] = append(resp[qType], ip)
}
res.IPList = nil
}
func decodeResult(dec *json.Decoder, ent *logEntry) { func decodeResult(dec *json.Decoder, ent *logEntry) {
defer translateResult(ent)
for { for {
keyToken, err := dec.Token() keyToken, err := dec.Token()
if err != nil { if err != nil {

View File

@ -36,7 +36,6 @@ func TestDecodeLogEntry(t *testing.T) {
`"Result":{` + `"Result":{` +
`"IsFiltered":true,` + `"IsFiltered":true,` +
`"Reason":3,` + `"Reason":3,` +
`"ReverseHosts":["example.net"],` +
`"IPList":["127.0.0.2"],` + `"IPList":["127.0.0.2"],` +
`"Rules":[{"FilterListID":42,"Text":"||an.yandex.ru","IP":"127.0.0.2"},` + `"Rules":[{"FilterListID":42,"Text":"||an.yandex.ru","IP":"127.0.0.2"},` +
`{"FilterListID":43,"Text":"||an2.yandex.ru","IP":"127.0.0.3"}],` + `{"FilterListID":43,"Text":"||an2.yandex.ru","IP":"127.0.0.3"}],` +
@ -60,7 +59,6 @@ func TestDecodeLogEntry(t *testing.T) {
Result: filtering.Result{ Result: filtering.Result{
IsFiltered: true, IsFiltered: true,
Reason: filtering.FilteredBlockList, Reason: filtering.FilteredBlockList,
ReverseHosts: []string{"example.net"},
IPList: []net.IP{net.IPv4(127, 0, 0, 2)}, IPList: []net.IP{net.IPv4(127, 0, 0, 2)},
Rules: []*filtering.ResultRule{{ Rules: []*filtering.ResultRule{{
FilterListID: 42, FilterListID: 42,
@ -170,8 +168,7 @@ func TestDecodeLogEntry(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
l := &logEntry{} decodeLogEntry(new(logEntry), tc.log)
decodeLogEntry(l, tc.log)
s := logOutput.String() s := logOutput.String()
if tc.want == "" { if tc.want == "" {
@ -185,3 +182,65 @@ func TestDecodeLogEntry(t *testing.T) {
}) })
} }
} }
func TestDecodeLogEntry_backwardCompatability(t *testing.T) {
var (
a1, a2 = net.IP{127, 0, 0, 1}.To16(), net.IP{127, 0, 0, 2}.To16()
aaaa1, aaaa2 = net.ParseIP("::1"), net.ParseIP("::2")
)
testCases := []struct {
want *logEntry
entry string
name string
}{{
entry: `{"Result":{"ReverseHosts":["example.net","example.org"]}`,
want: &logEntry{
Result: filtering.Result{DNSRewriteResult: &filtering.DNSRewriteResult{
RCode: dns.RcodeSuccess,
Response: filtering.DNSRewriteResultResponse{
dns.TypePTR: []rules.RRValue{"example.net.", "example.org."},
},
}},
},
name: "reverse_hosts",
}, {
entry: `{"Result":{"IPList":["127.0.0.1","127.0.0.2","::1","::2"],"Reason":10}}`,
want: &logEntry{
Result: filtering.Result{
DNSRewriteResult: &filtering.DNSRewriteResult{
RCode: dns.RcodeSuccess,
Response: filtering.DNSRewriteResultResponse{
dns.TypeA: []rules.RRValue{a1, a2},
dns.TypeAAAA: []rules.RRValue{aaaa1, aaaa2},
},
},
Reason: filtering.RewrittenAutoHosts,
},
},
name: "iplist_autohosts",
}, {
entry: `{"Result":{"IPList":["127.0.0.1","127.0.0.2","::1","::2"],"Reason":9}}`,
want: &logEntry{
Result: filtering.Result{
IPList: []net.IP{
a1,
a2,
aaaa1,
aaaa2,
},
Reason: filtering.Rewritten,
},
},
name: "iplist_rewritten",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := &logEntry{}
decodeLogEntry(e, tc.entry)
assert.Equal(t, tc.want, e)
})
}
}