Pull request: 4079 fix hosts container aliases

Merge in DNS/adguard-home from 4079-hosts-again to master

Updates #4079.

Squashed commit of the following:

commit 6aa8cbf32e8e47ba46bf5fba7681a10b68b4bc01
Merge: 19dba371 34c95f99
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jan 12 14:05:30 2022 +0500

    Merge branch 'master' into 4079-hosts-again

commit 19dba371cc30ab8b75b0116833f4ecf0ef0f182f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jan 12 14:05:20 2022 +0500

    aghnet: imp docs

commit 9f341eb8ee4ba8468240bc3eeeb4951a3f7f5e6d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Jan 10 18:44:17 2022 +0500

    aghnet: fix races

commit fd66191c7637c8584711e5bb8186494327ce0f87
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Jan 6 17:21:14 2022 +0500

    aghnet: fix hosts container aliases
This commit is contained in:
Eugene Burkov 2022-01-12 12:14:59 +03:00
parent 34c95f99f8
commit 1458600c37
5 changed files with 314 additions and 177 deletions

View File

@ -27,6 +27,11 @@ TODO(a.garipov): Remove this deprecation, if v0.108.0 is released before the Go
--> -->
- Go 1.17 support. v0.109.0 will require at least Go 1.18 to build. - Go 1.17 support. v0.109.0 will require at least Go 1.18 to build.
### Fixed
- Omitted aliases of hosts specified by another line within the OS's hosts file
([#4079]).
### Removed ### Removed
- Go 1.16 support. - Go 1.16 support.
@ -77,6 +82,7 @@ TODO(a.garipov): Remove this deprecation, if v0.108.0 is released before the Go
[#4008]: https://github.com/AdguardTeam/AdGuardHome/issues/4008 [#4008]: https://github.com/AdguardTeam/AdGuardHome/issues/4008
[#4016]: https://github.com/AdguardTeam/AdGuardHome/issues/4016 [#4016]: https://github.com/AdguardTeam/AdGuardHome/issues/4016
[#4027]: https://github.com/AdguardTeam/AdGuardHome/issues/4027 [#4027]: https://github.com/AdguardTeam/AdGuardHome/issues/4027
[#4079]: https://github.com/AdguardTeam/AdGuardHome/issues/4079

View File

@ -72,7 +72,11 @@ func (rm *requestMatcher) MatchRequest(
} }
// Translate returns the source hosts-syntax rule for the generated dnsrewrite // Translate returns the source hosts-syntax rule for the generated dnsrewrite
// rule or an empty string if the last doesn't exist. // rule or an empty string if the last doesn't exist. The returned rules are in
// a processed format like:
//
// ip host1 host2 ...
//
func (rm *requestMatcher) Translate(rule string) (hostRule string) { func (rm *requestMatcher) Translate(rule string) (hostRule string) {
rm.stateLock.RLock() rm.stateLock.RLock()
defer rm.stateLock.RUnlock() defer rm.stateLock.RUnlock()
@ -179,7 +183,7 @@ func NewHostsContainer(
return nil, fmt.Errorf("adding path: %w", err) return nil, fmt.Errorf("adding path: %w", err)
} }
log.Debug("%s: file %q expected to exist but doesn't", hostsContainerPref, p) log.Debug("%s: %s is expected to exist but doesn't", hostsContainerPref, p)
} }
} }
@ -199,7 +203,7 @@ func (hc *HostsContainer) Close() (err error) {
} }
// Upd returns the channel into which the updates are sent. The receivable // Upd returns the channel into which the updates are sent. The receivable
// map's values are guaranteed to be of type of *stringutil.Set. // map's values are guaranteed to be of type of *aghnet.Hosts.
func (hc *HostsContainer) Upd() (updates <-chan *netutil.IPMap) { func (hc *HostsContainer) Upd() (updates <-chan *netutil.IPMap) {
return hc.updates return hc.updates
} }
@ -228,8 +232,9 @@ func pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error)
return patterns, nil return patterns, nil
} }
// handleEvents concurrently handles the events. It closes the update channel // handleEvents concurrently handles the file system events. It closes the
// of HostsContainer when finishes. Used to be called within a goroutine. // update channel of HostsContainer when finishes. It's used to be called
// within a separate goroutine.
func (hc *HostsContainer) handleEvents() { func (hc *HostsContainer) handleEvents() {
defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPref)) defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPref))
@ -254,17 +259,27 @@ func (hc *HostsContainer) handleEvents() {
} }
} }
// ipRules is the pair of generated A/AAAA and PTR rules with related IP.
type ipRules struct {
// rule is the A/AAAA $dnsrewrite rule.
rule string
// rulePtr is the PTR $dnsrewrite rule.
rulePtr string
// ip is the IP address related to the rules.
ip net.IP
}
// 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. It exists for only a single refreshing session. // file. It exists for only a single refreshing session.
type hostsParser struct { type hostsParser struct {
// rulesBuilder builds the resulting rulesBuilder list content. // rulesBuilder builds the resulting rules list content.
rulesBuilder *strings.Builder rulesBuilder *strings.Builder
// translations maps generated $dnsrewrite rules to the hosts-translations // rules stores the rules for main hosts to generate translations.
// rules. rules []ipRules
translations map[string]string
// cnameSet prevents duplicating cname rules. // cnameSet prevents duplicating cname rules, e.g. same hostname for
// different IP versions.
cnameSet *stringutil.Set 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
@ -272,13 +287,16 @@ type hostsParser struct {
table *netutil.IPMap table *netutil.IPMap
} }
// newHostsParser creates a new *hostsParser with buffers of size taken from the
// previous parse.
func (hc *HostsContainer) newHostsParser() (hp *hostsParser) { func (hc *HostsContainer) newHostsParser() (hp *hostsParser) {
lastLen := hc.last.Len()
return &hostsParser{ return &hostsParser{
rulesBuilder: &strings.Builder{}, rulesBuilder: &strings.Builder{},
// For A/AAAA and PTRs. rules: make([]ipRules, 0, lastLen),
translations: make(map[string]string, hc.last.Len()*2),
cnameSet: stringutil.NewSet(), cnameSet: stringutil.NewSet(),
table: netutil.NewIPMap(hc.last.Len()), table: netutil.NewIPMap(lastLen),
} }
} }
@ -286,9 +304,7 @@ func (hc *HostsContainer) newHostsParser() (hp *hostsParser) {
// never signs to stop walking and never returns any additional patterns. // never signs to stop walking and never returns any additional patterns.
// //
// See man hosts(5). // See man hosts(5).
func (hp *hostsParser) parseFile( func (hp *hostsParser) parseFile(r io.Reader) (patterns []string, cont bool, err error) {
r io.Reader,
) (patterns []string, cont bool, err error) {
s := bufio.NewScanner(r) s := bufio.NewScanner(r)
for s.Scan() { for s.Scan() {
ip, hosts := hp.parseLine(s.Text()) ip, hosts := hp.parseLine(s.Text())
@ -339,62 +355,79 @@ func (hp *hostsParser) parseLine(line string) (ip net.IP, hosts []string) {
return ip, hosts return ip, hosts
} }
// Simple types of hosts in hosts database. Zero value isn't used to be able // Hosts is used to contain the main host and all it's aliases.
// quizzaciously emulate nil with 0. type Hosts struct {
const ( // Aliases contains all the aliases for Main.
_ = iota Aliases *stringutil.Set
hostAlias // Main is the host itself.
hostMain Main string
) }
// Equal returns true if h equals hh.
func (h *Hosts) Equal(hh *Hosts) (ok bool) {
if h == nil || hh == nil {
return h == hh
}
return h.Main == hh.Main && h.Aliases.Equal(hh.Aliases)
}
// add tries to add the ip-host pair. It returns: // add tries to add the ip-host pair. It returns:
// //
// hostAlias if the host is not the first one added for the ip. // main host if the host is not the first one added for the ip.
// hostMain if the host is the first one added for the ip. // host itself if the host is the first one added for the ip.
// 0 if the ip-host pair has already been added. // "" if the ip-host pair has already been added.
// //
func (hp *hostsParser) add(ip net.IP, host string) (hostType int) { func (hp *hostsParser) add(ip net.IP, host string) (mainHost string) {
v, ok := hp.table.Get(ip) v, ok := hp.table.Get(ip)
switch hosts, _ := v.(*stringutil.Set); { switch h, _ := v.(*Hosts); {
case ok && hosts.Has(host): case !ok:
return 0 // This is the first host for the ip.
case hosts == nil: hp.table.Set(ip, &Hosts{Main: host})
hosts = stringutil.NewSet(host)
hp.table.Set(ip, hosts)
return hostMain return host
case h.Main == host:
// This is a duplicate. Go on.
case h.Aliases == nil:
// This is the first alias.
h.Aliases = stringutil.NewSet(host)
return h.Main
case !h.Aliases.Has(host):
// This is a new alias.
h.Aliases.Add(host)
return h.Main
default: default:
hosts.Add(host) // This is a duplicate. Go on.
return hostAlias
} }
return ""
} }
// addPair puts the pair of ip and host to the rules builder if needed. For // 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. // each ip the first member of hosts will become the main one.
func (hp *hostsParser) addPairs(ip net.IP, hosts []string) { func (hp *hostsParser) addPairs(ip net.IP, hosts []string) {
// Put the rule in a processed format like:
//
// ip host1 host2 ...
//
hostsLine := strings.Join(append([]string{ip.String()}, hosts...), " ")
var mainHost string
for _, host := range hosts { for _, host := range hosts {
switch hp.add(ip, host) { switch mainHost := hp.add(ip, host); mainHost {
case 0: case "":
// This host is a duplicate.
continue continue
case hostMain: case host:
mainHost = host // This host is main.
added, addedPtr := hp.writeMainHostRule(host, ip) added, addedPtr := hp.writeMainRule(host, ip)
hp.translations[added], hp.translations[addedPtr] = hostsLine, hostsLine hp.rules = append(hp.rules, ipRules{
case hostAlias: rule: added,
rulePtr: addedPtr,
ip: ip,
})
default:
// This host is an alias.
pair := fmt.Sprint(host, " ", mainHost) pair := fmt.Sprint(host, " ", mainHost)
if hp.cnameSet.Has(pair) { if hp.cnameSet.Has(pair) {
continue continue
} }
// Since the hostAlias couldn't be returned from add before the hp.writeAliasRule(host, mainHost)
// hostMain the mainHost shouldn't appear empty.
hp.writeAliasHostRule(host, mainHost)
hp.cnameSet.Add(pair) hp.cnameSet.Add(pair)
} }
@ -402,9 +435,9 @@ func (hp *hostsParser) addPairs(ip net.IP, hosts []string) {
} }
} }
// writeAliasHostRule writes the CNAME rule for the alias-host pair into // writeAliasRule writes the CNAME rule for the alias-host pair into internal
// internal builders. // builders.
func (hp *hostsParser) writeAliasHostRule(alias, host string) { func (hp *hostsParser) writeAliasRule(alias, host string) {
const ( const (
nl = "\n" nl = "\n"
sc = ";" sc = ";"
@ -417,9 +450,9 @@ func (hp *hostsParser) writeAliasHostRule(alias, host string) {
stringutil.WriteToBuilder(hp.rulesBuilder, rules.MaskPipe, alias, rwSuccess, host, nl) stringutil.WriteToBuilder(hp.rulesBuilder, rules.MaskPipe, alias, rwSuccess, host, nl)
} }
// writeMainHostRule writes the actual rule for the qtype and the PTR for the // writeMainRule writes the actual rule for the qtype and the PTR for the
// host-ip pair into internal builders. // host-ip pair into internal builders.
func (hp *hostsParser) writeMainHostRule(host string, ip net.IP) (added, addedPtr string) { func (hp *hostsParser) writeMainRule(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
@ -484,12 +517,15 @@ func (hp *hostsParser) equalSet(target *netutil.IPMap) (ok bool) {
return false return false
} }
hp.table.Range(func(ip net.IP, val interface{}) (cont bool) { hp.table.Range(func(ip net.IP, b interface{}) (cont bool) {
v, hasIP := target.Get(ip)
// ok is set to true if the target doesn't contain ip or if the // ok is set to true if the target doesn't contain ip or if the
// appropriate hosts set isn't equal to the checked one, i.e. the maps // appropriate hosts set isn't equal to the checked one, i.e. the main
// have at least one discrepancy. // hosts differ or the maps have at least one discrepancy.
ok = !hasIP || !v.(*stringutil.Set).Equal(val.(*stringutil.Set)) if a, hasIP := target.Get(ip); !hasIP {
ok = true
} else if hosts, aok := a.(*Hosts); aok {
ok = !hosts.Equal(b.(*Hosts))
}
// Continue only if maps has no discrepancies. // Continue only if maps has no discrepancies.
return !ok return !ok
@ -527,6 +563,35 @@ func (hp *hostsParser) newStrg(id int) (s *filterlist.RuleStorage, err error) {
}}) }})
} }
// translations generates the map to translate $dnsrewrite rules to
// hosts-syntax ones.
func (hp *hostsParser) translations() (trans map[string]string) {
l := len(hp.rules)
if l == 0 {
return nil
}
trans = make(map[string]string, l*2)
for _, r := range hp.rules {
v, ok := hp.table.Get(r.ip)
if !ok {
continue
}
var hosts *Hosts
hosts, ok = v.(*Hosts)
if !ok {
continue
}
strs := append([]string{r.ip.String(), hosts.Main}, hosts.Aliases.Values()...)
hostsLine := strings.Join(strs, " ")
trans[r.rule], trans[r.rulePtr] = hostsLine, hostsLine
}
return trans
}
// refresh gets the data from specified files and propagates the updates if // refresh gets the data from specified files and propagates the updates if
// needed. // needed.
// //
@ -540,7 +605,7 @@ func (hc *HostsContainer) refresh() (err error) {
} }
if hp.equalSet(hc.last) { if hp.equalSet(hc.last) {
log.Debug("%s: no updates detected", hostsContainerPref) log.Debug("%s: no changes detected", hostsContainerPref)
return nil return nil
} }
@ -553,7 +618,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, hp.translations) hc.resetEng(rulesStrg, hp.translations())
return nil return nil
} }

View File

@ -9,6 +9,7 @@ import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"testing/fstest" "testing/fstest"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
@ -129,24 +130,13 @@ func TestNewHostsContainer(t *testing.T) {
}) })
} }
func TestHostsContainer_Refresh(t *testing.T) { func TestHostsContainer_refresh(t *testing.T) {
knownIP := net.IP{127, 0, 0, 1} // TODO(e.burkov): Test the case with no actual updates.
const knownHost = "localhost" ip := net.IP{127, 0, 0, 1}
const knownAlias = "hocallost" ipStr := ip.String()
const dirname = "dir" testFS := fstest.MapFS{"dir/file1": &fstest.MapFile{Data: []byte(ipStr + ` hostname` + nl)}}
const filename1 = "file1"
const filename2 = "file2"
p1 := path.Join(dirname, filename1)
p2 := path.Join(dirname, filename2)
testFS := fstest.MapFS{
p1: &fstest.MapFile{
Data: []byte(strings.Join([]string{knownIP.String(), knownHost}, sp) + nl),
},
}
// event is a convenient alias for an empty struct{} to emit test events. // event is a convenient alias for an empty struct{} to emit test events.
type event = struct{} type event = struct{}
@ -157,119 +147,117 @@ func TestHostsContainer_Refresh(t *testing.T) {
w := &aghtest.FSWatcher{ w := &aghtest.FSWatcher{
OnEvents: func() (e <-chan event) { return eventsCh }, OnEvents: func() (e <-chan event) { return eventsCh },
OnAdd: func(name string) (err error) { OnAdd: func(name string) (err error) {
assert.Equal(t, dirname, name) assert.Equal(t, "dir", name)
return nil return nil
}, },
OnClose: func() (err error) { panic("not implemented") }, OnClose: func() (err error) { panic("not implemented") },
} }
hc, err := NewHostsContainer(0, testFS, w, dirname) hc, err := NewHostsContainer(0, testFS, w, "dir")
require.NoError(t, err) require.NoError(t, err)
checkRefresh := func(t *testing.T, wantHosts *stringutil.Set) { checkRefresh := func(t *testing.T, wantHosts Hosts) {
upd, ok := <-hc.Upd() upd, ok := <-hc.Upd()
require.True(t, ok) require.True(t, ok)
require.NotNil(t, upd) require.NotNil(t, upd)
assert.Equal(t, 1, upd.Len()) assert.Equal(t, 1, upd.Len())
v, ok := upd.Get(knownIP) v, ok := upd.Get(ip)
require.True(t, ok) require.True(t, ok)
var hosts *stringutil.Set var hosts *Hosts
hosts, ok = v.(*stringutil.Set) hosts, ok = v.(*Hosts)
require.True(t, ok) require.True(t, ok)
assert.True(t, hosts.Equal(wantHosts)) assert.Equal(t, wantHosts.Main, hosts.Main)
assert.True(t, hosts.Aliases.Equal(wantHosts.Aliases))
} }
t.Run("initial_refresh", func(t *testing.T) { t.Run("initial_refresh", func(t *testing.T) {
checkRefresh(t, stringutil.NewSet(knownHost)) checkRefresh(t, Hosts{Main: "hostname"})
}) })
testFS[p2] = &fstest.MapFile{
Data: []byte(strings.Join([]string{knownIP.String(), knownAlias}, sp) + nl),
}
eventsCh <- event{}
t.Run("second_refresh", func(t *testing.T) { t.Run("second_refresh", func(t *testing.T) {
checkRefresh(t, stringutil.NewSet(knownHost, knownAlias)) testFS["dir/file2"] = &fstest.MapFile{Data: []byte(ipStr + ` alias` + nl)}
eventsCh <- event{}
checkRefresh(t, Hosts{Main: "hostname", Aliases: stringutil.NewSet("alias")})
}) })
eventsCh <- event{} t.Run("double_refresh", func(t *testing.T) {
// Make a change once.
testFS["dir/file1"] = &fstest.MapFile{Data: []byte(ipStr + ` alias` + nl)}
eventsCh <- event{}
t.Run("no_changes_refresh", func(t *testing.T) { // Require the changes are written.
assert.Empty(t, hc.Upd()) require.Eventually(t, func() bool {
res, ok := hc.MatchRequest(urlfilter.DNSRequest{
Hostname: "hostname",
DNSType: dns.TypeA,
})
return !ok && res.DNSRewrites() == nil
}, 5*time.Second, time.Second/2)
// Make a change again.
testFS["dir/file2"] = &fstest.MapFile{Data: []byte(ipStr + ` hostname` + nl)}
eventsCh <- event{}
// Require the changes are written.
require.Eventually(t, func() bool {
res, ok := hc.MatchRequest(urlfilter.DNSRequest{
Hostname: "hostname",
DNSType: dns.TypeA,
})
return !ok && res.DNSRewrites() != nil
}, 5*time.Second, time.Second/2)
assert.Len(t, hc.Upd(), 1)
}) })
} }
func TestHostsContainer_PathsToPatterns(t *testing.T) { func TestHostsContainer_PathsToPatterns(t *testing.T) {
const (
dir0 = "dir"
dir1 = "dir_1"
fn1 = "file_1"
fn2 = "file_2"
fn3 = "file_3"
fn4 = "file_4"
)
fp1 := path.Join(dir0, fn1)
fp2 := path.Join(dir0, fn2)
fp3 := path.Join(dir0, dir1, fn3)
gsfs := fstest.MapFS{ gsfs := fstest.MapFS{
fp1: &fstest.MapFile{Data: []byte{1}}, "dir_0/file_1": &fstest.MapFile{Data: []byte{1}},
fp2: &fstest.MapFile{Data: []byte{2}}, "dir_0/file_2": &fstest.MapFile{Data: []byte{2}},
fp3: &fstest.MapFile{Data: []byte{3}}, "dir_0/dir_1/file_3": &fstest.MapFile{Data: []byte{3}},
} }
testCases := []struct { testCases := []struct {
name string name string
wantErr error paths []string
want []string want []string
paths []string
}{{ }{{
name: "no_paths", name: "no_paths",
wantErr: nil, paths: nil,
want: nil, want: nil,
paths: nil,
}, { }, {
name: "single_file", name: "single_file",
wantErr: nil, paths: []string{"dir_0/file_1"},
want: []string{fp1}, want: []string{"dir_0/file_1"},
paths: []string{fp1},
}, { }, {
name: "several_files", name: "several_files",
wantErr: nil, paths: []string{"dir_0/file_1", "dir_0/file_2"},
want: []string{fp1, fp2}, want: []string{"dir_0/file_1", "dir_0/file_2"},
paths: []string{fp1, fp2},
}, { }, {
name: "whole_dir", name: "whole_dir",
wantErr: nil, paths: []string{"dir_0"},
want: []string{path.Join(dir0, "*")}, want: []string{"dir_0/*"},
paths: []string{dir0},
}, { }, {
name: "file_and_dir", name: "file_and_dir",
wantErr: nil, paths: []string{"dir_0/file_1", "dir_0/dir_1"},
want: []string{fp1, path.Join(dir0, dir1, "*")}, want: []string{"dir_0/file_1", "dir_0/dir_1/*"},
paths: []string{fp1, path.Join(dir0, dir1)},
}, { }, {
name: "non-existing", name: "non-existing",
wantErr: nil, paths: []string{path.Join("dir_0", "file_3")},
want: nil, want: nil,
paths: []string{path.Join(dir0, "file_3")},
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
patterns, err := pathsToPatterns(gsfs, tc.paths) patterns, err := pathsToPatterns(gsfs, tc.paths)
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
return
}
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.want, patterns) assert.Equal(t, tc.want, patterns)
@ -290,16 +278,74 @@ func TestHostsContainer_PathsToPatterns(t *testing.T) {
}) })
} }
func TestHostsContainer_Translate(t *testing.T) {
testdata := os.DirFS("./testdata")
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(0, testdata, &stubWatcher, "etc_hosts")
require.NoError(t, err)
testCases := []struct {
name string
rule string
wantTrans []string
}{{
name: "simplehost",
rule: "|simplehost^$dnsrewrite=NOERROR;A;1.0.0.1",
wantTrans: []string{"1.0.0.1", "simplehost"},
}, {
name: "hello",
rule: "|hello^$dnsrewrite=NOERROR;A;1.0.0.0",
wantTrans: []string{"1.0.0.0", "hello", "hello.world", "hello.world.again"},
}, {
name: "simplehost_v6",
rule: "|simplehost^$dnsrewrite=NOERROR;AAAA;::1",
wantTrans: []string{"::1", "simplehost"},
}, {
name: "hello_v6",
rule: "|hello^$dnsrewrite=NOERROR;AAAA;::",
wantTrans: []string{"::", "hello", "hello.world", "hello.world.again"},
}, {
name: "simplehost_ptr",
rule: "|1.0.0.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;simplehost.",
wantTrans: []string{"1.0.0.1", "simplehost"},
}, {
name: "hello_ptr",
rule: "|0.0.0.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;hello.",
wantTrans: []string{"1.0.0.0", "hello", "hello.world", "hello.world.again"},
}, {
name: "simplehost_ptr_v6",
rule: "|1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" +
"^$dnsrewrite=NOERROR;PTR;simplehost.",
wantTrans: []string{"::1", "simplehost"},
}, {
name: "hello_ptr_v6",
rule: "|0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" +
"^$dnsrewrite=NOERROR;PTR;hello.",
wantTrans: []string{"::", "hello", "hello.world", "hello.world.again"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
want := stringutil.NewSet(tc.wantTrans...)
got := stringutil.NewSet(strings.Fields(hc.Translate(tc.rule))...)
assert.True(t, want.Equal(got))
})
}
}
func TestHostsContainer(t *testing.T) { func TestHostsContainer(t *testing.T) {
const listID = 1234 const listID = 1234
testdata := os.DirFS("./testdata") testdata := os.DirFS("./testdata")
nRewrites := func(t *testing.T, res *urlfilter.DNSResult, n int) (rws []*rules.DNSRewrite) { nRewrites := func(t *testing.T, res *urlfilter.DNSResult, n int) (rws []*rules.DNSRewrite) {
t.Helper()
rewrites := res.DNSRewrites() rewrites := res.DNSRewrites()
assert.Len(t, rewrites, n) require.Len(t, rewrites, n)
for _, rewrite := range rewrites { for _, rewrite := range rewrites {
require.Equal(t, listID, rewrite.FilterListID) require.Equal(t, listID, rewrite.FilterListID)
@ -346,6 +392,15 @@ func TestHostsContainer(t *testing.T) {
testTail: func(t *testing.T, res *urlfilter.DNSResult) { testTail: func(t *testing.T, res *urlfilter.DNSResult) {
assert.Equal(t, "hello", nRewrites(t, res, 1)[0].NewCNAME) assert.Equal(t, "hello", nRewrites(t, res, 1)[0].NewCNAME)
}, },
}, {
name: "other_line_alias",
req: urlfilter.DNSRequest{
Hostname: "hello.world.again",
DNSType: dns.TypeA,
},
testTail: func(t *testing.T, res *urlfilter.DNSResult) {
assert.Equal(t, "hello", nRewrites(t, res, 1)[0].NewCNAME)
},
}, { }, {
name: "hello_subdomain", name: "hello_subdomain",
req: urlfilter.DNSRequest{ req: urlfilter.DNSRequest{
@ -419,12 +474,8 @@ func TestHostsContainer(t *testing.T) {
} }
func TestUniqueRules_ParseLine(t *testing.T) { func TestUniqueRules_ParseLine(t *testing.T) {
const ( ip := net.IP{127, 0, 0, 1}
hostname = "localhost" ipStr := ip.String()
alias = "hocallost"
)
knownIP := net.IP{127, 0, 0, 1}
testCases := []struct { testCases := []struct {
name string name string
@ -433,39 +484,39 @@ func TestUniqueRules_ParseLine(t *testing.T) {
wantHosts []string wantHosts []string
}{{ }{{
name: "simple", name: "simple",
line: strings.Join([]string{knownIP.String(), hostname}, sp), line: ipStr + ` hostname`,
wantIP: knownIP, wantIP: ip,
wantHosts: []string{"localhost"}, wantHosts: []string{"hostname"},
}, { }, {
name: "aliases", name: "aliases",
line: strings.Join([]string{knownIP.String(), hostname, alias}, sp), line: ipStr + ` hostname alias`,
wantIP: knownIP, wantIP: ip,
wantHosts: []string{"localhost", "hocallost"}, wantHosts: []string{"hostname", "alias"},
}, { }, {
name: "invalid_line", name: "invalid_line",
line: knownIP.String(), line: ipStr,
wantIP: nil, wantIP: nil,
wantHosts: nil, wantHosts: nil,
}, { }, {
name: "invalid_line_hostname", name: "invalid_line_hostname",
line: strings.Join([]string{knownIP.String(), "#" + hostname}, sp), line: ipStr + ` # hostname`,
wantIP: knownIP, wantIP: ip,
wantHosts: nil, wantHosts: nil,
}, { }, {
name: "commented_aliases", name: "commented_aliases",
line: strings.Join([]string{knownIP.String(), hostname, "#" + alias}, sp), line: ipStr + ` hostname # alias`,
wantIP: knownIP, wantIP: ip,
wantHosts: []string{"localhost"}, wantHosts: []string{"hostname"},
}, { }, {
name: "whole_comment", name: "whole_comment",
line: strings.Join([]string{"#", knownIP.String(), hostname}, sp), line: `# ` + ipStr + ` hostname`,
wantIP: nil, wantIP: nil,
wantHosts: nil, wantHosts: nil,
}, { }, {
name: "partial_comment", name: "partial_comment",
line: strings.Join([]string{knownIP.String(), hostname[:4] + "#" + hostname[4:]}, sp), line: ipStr + ` host#name`,
wantIP: knownIP, wantIP: ip,
wantHosts: []string{hostname[:4]}, wantHosts: []string{"host"},
}, { }, {
name: "empty", name: "empty",
line: ``, line: ``,
@ -476,8 +527,8 @@ func TestUniqueRules_ParseLine(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
hp := hostsParser{} hp := hostsParser{}
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
ip, hosts := hp.parseLine(tc.line) got, hosts := hp.parseLine(tc.line)
assert.True(t, tc.wantIP.Equal(ip)) assert.True(t, tc.wantIP.Equal(got))
assert.Equal(t, tc.wantHosts, hosts) assert.Equal(t, tc.wantHosts, hosts)
}) })
} }

View File

@ -12,9 +12,19 @@
1.0.0.3 * 1.0.0.3 *
1.0.0.4 *.com 1.0.0.4 *.com
# See https://github.com/AdguardTeam/AdGuardHome/issues/4079.
1.0.0.0 hello.world.again
# Duplicates of a main host and an alias.
1.0.0.1 simplehost
1.0.0.0 hello.world
# Same for IPv6. # Same for IPv6.
::1 simplehost ::1 simplehost
:: hello hello.world :: hello hello.world
::2 a.whole lot.of aliases for.testing ::2 a.whole lot.of aliases for.testing
::3 * ::3 *
::4 *.com ::4 *.com
:: hello.world.again
::1 simplehost
:: hello.world

View File

@ -783,12 +783,17 @@ func (clients *clientsContainer) addFromHostsFile(hosts *netutil.IPMap) {
n := 0 n := 0
hosts.Range(func(ip net.IP, v interface{}) (cont bool) { hosts.Range(func(ip net.IP, v interface{}) (cont bool) {
names, ok := v.(*stringutil.Set) hosts, ok := v.(*aghnet.Hosts)
if !ok { if !ok {
log.Error("dns: bad type %T in ipToRC for %s", v, ip) log.Error("dns: bad type %T in ipToRC for %s", v, ip)
return true
} }
names.Range(func(name string) (cont bool) { if clients.addHostLocked(ip, hosts.Main, ClientSourceHostsFile) {
n++
}
hosts.Aliases.Range(func(name string) (cont bool) {
if clients.addHostLocked(ip, name, ClientSourceHostsFile) { if clients.addHostLocked(ip, name, ClientSourceHostsFile) {
n++ n++
} }