From 1458600c37dba16003518685729c7212598fe8ad Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Wed, 12 Jan 2022 12:14:59 +0300 Subject: [PATCH] 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 Date: Wed Jan 12 14:05:30 2022 +0500 Merge branch 'master' into 4079-hosts-again commit 19dba371cc30ab8b75b0116833f4ecf0ef0f182f Author: Eugene Burkov Date: Wed Jan 12 14:05:20 2022 +0500 aghnet: imp docs commit 9f341eb8ee4ba8468240bc3eeeb4951a3f7f5e6d Author: Eugene Burkov Date: Mon Jan 10 18:44:17 2022 +0500 aghnet: fix races commit fd66191c7637c8584711e5bb8186494327ce0f87 Author: Eugene Burkov Date: Thu Jan 6 17:21:14 2022 +0500 aghnet: fix hosts container aliases --- CHANGELOG.md | 6 + internal/aghnet/hostscontainer.go | 195 ++++++++++++------ internal/aghnet/hostscontainer_test.go | 271 +++++++++++++++---------- internal/aghnet/testdata/etc_hosts | 10 + internal/home/clients.go | 9 +- 5 files changed, 314 insertions(+), 177 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b53e9f14..fdb58899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. +### Fixed + +- Omitted aliases of hosts specified by another line within the OS's hosts file + ([#4079]). + ### Removed - 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 [#4016]: https://github.com/AdguardTeam/AdGuardHome/issues/4016 [#4027]: https://github.com/AdguardTeam/AdGuardHome/issues/4027 +[#4079]: https://github.com/AdguardTeam/AdGuardHome/issues/4079 diff --git a/internal/aghnet/hostscontainer.go b/internal/aghnet/hostscontainer.go index 436464e2..85379fed 100644 --- a/internal/aghnet/hostscontainer.go +++ b/internal/aghnet/hostscontainer.go @@ -72,7 +72,11 @@ func (rm *requestMatcher) MatchRequest( } // 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) { rm.stateLock.RLock() defer rm.stateLock.RUnlock() @@ -179,7 +183,7 @@ func NewHostsContainer( 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 -// 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) { return hc.updates } @@ -228,8 +232,9 @@ func pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error) return patterns, nil } -// handleEvents concurrently handles the events. It closes the update channel -// of HostsContainer when finishes. Used to be called within a goroutine. +// handleEvents concurrently handles the file system events. It closes the +// update channel of HostsContainer when finishes. It's used to be called +// within a separate goroutine. func (hc *HostsContainer) handleEvents() { 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 // file. It exists for only a single refreshing session. type hostsParser struct { - // rulesBuilder builds the resulting rulesBuilder list content. + // rulesBuilder builds the resulting rules list content. rulesBuilder *strings.Builder - // translations maps generated $dnsrewrite rules to the hosts-translations - // rules. - translations map[string]string + // rules stores the rules for main hosts to generate translations. + rules []ipRules - // cnameSet prevents duplicating cname rules. + // cnameSet prevents duplicating cname rules, e.g. same hostname for + // different IP versions. cnameSet *stringutil.Set // table stores only the unique IP-hostname pairs. It's also sent to the @@ -272,13 +287,16 @@ type hostsParser struct { table *netutil.IPMap } +// newHostsParser creates a new *hostsParser with buffers of size taken from the +// previous parse. func (hc *HostsContainer) newHostsParser() (hp *hostsParser) { + lastLen := hc.last.Len() + return &hostsParser{ rulesBuilder: &strings.Builder{}, - // For A/AAAA and PTRs. - translations: make(map[string]string, hc.last.Len()*2), + rules: make([]ipRules, 0, lastLen), 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. // // See man hosts(5). -func (hp *hostsParser) parseFile( - r io.Reader, -) (patterns []string, cont bool, err error) { +func (hp *hostsParser) parseFile(r io.Reader) (patterns []string, cont bool, err error) { s := bufio.NewScanner(r) for s.Scan() { ip, hosts := hp.parseLine(s.Text()) @@ -339,62 +355,79 @@ func (hp *hostsParser) parseLine(line string) (ip net.IP, hosts []string) { return ip, hosts } -// Simple types of hosts in hosts database. Zero value isn't used to be able -// quizzaciously emulate nil with 0. -const ( - _ = iota - hostAlias - hostMain -) +// Hosts is used to contain the main host and all it's aliases. +type Hosts struct { + // Aliases contains all the aliases for Main. + Aliases *stringutil.Set + // Main is the host itself. + 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: // -// 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. +// main host if the host is not the first one added for the ip. +// host itself if the host is the first one added for the ip. +// "" 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) - switch hosts, _ := v.(*stringutil.Set); { - case ok && hosts.Has(host): - return 0 - case hosts == nil: - hosts = stringutil.NewSet(host) - hp.table.Set(ip, hosts) + switch h, _ := v.(*Hosts); { + case !ok: + // This is the first host for the ip. + hp.table.Set(ip, &Hosts{Main: host}) - 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: - hosts.Add(host) - - return hostAlias + // This is a duplicate. Go on. } + + return "" } // 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 processed 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: + switch mainHost := hp.add(ip, host); mainHost { + case "": + // This host is a duplicate. continue - case hostMain: - mainHost = host - added, addedPtr := hp.writeMainHostRule(host, ip) - hp.translations[added], hp.translations[addedPtr] = hostsLine, hostsLine - case hostAlias: + case host: + // This host is main. + added, addedPtr := hp.writeMainRule(host, ip) + hp.rules = append(hp.rules, ipRules{ + rule: added, + rulePtr: addedPtr, + ip: ip, + }) + default: + // This host is an alias. 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.writeAliasRule(host, mainHost) 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 -// internal builders. -func (hp *hostsParser) writeAliasHostRule(alias, host string) { +// writeAliasRule writes the CNAME rule for the alias-host pair into internal +// builders. +func (hp *hostsParser) writeAliasRule(alias, host string) { const ( nl = "\n" sc = ";" @@ -417,9 +450,9 @@ func (hp *hostsParser) writeAliasHostRule(alias, host string) { 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. -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) if err != nil { return @@ -484,12 +517,15 @@ func (hp *hostsParser) equalSet(target *netutil.IPMap) (ok bool) { return false } - hp.table.Range(func(ip net.IP, val interface{}) (cont bool) { - v, hasIP := target.Get(ip) + hp.table.Range(func(ip net.IP, b interface{}) (cont bool) { // 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 - // have at least one discrepancy. - ok = !hasIP || !v.(*stringutil.Set).Equal(val.(*stringutil.Set)) + // appropriate hosts set isn't equal to the checked one, i.e. the main + // hosts differ or the maps have at least one discrepancy. + 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. 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 // needed. // @@ -540,7 +605,7 @@ func (hc *HostsContainer) refresh() (err error) { } if hp.equalSet(hc.last) { - log.Debug("%s: no updates detected", hostsContainerPref) + log.Debug("%s: no changes detected", hostsContainerPref) return nil } @@ -553,7 +618,7 @@ func (hc *HostsContainer) refresh() (err error) { return fmt.Errorf("initializing rules storage: %w", err) } - hc.resetEng(rulesStrg, hp.translations) + hc.resetEng(rulesStrg, hp.translations()) return nil } diff --git a/internal/aghnet/hostscontainer_test.go b/internal/aghnet/hostscontainer_test.go index e2784908..70f2d00f 100644 --- a/internal/aghnet/hostscontainer_test.go +++ b/internal/aghnet/hostscontainer_test.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "testing" "testing/fstest" + "time" "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/golibs/errors" @@ -129,24 +130,13 @@ func TestNewHostsContainer(t *testing.T) { }) } -func TestHostsContainer_Refresh(t *testing.T) { - knownIP := net.IP{127, 0, 0, 1} +func TestHostsContainer_refresh(t *testing.T) { + // TODO(e.burkov): Test the case with no actual updates. - const knownHost = "localhost" - const knownAlias = "hocallost" + ip := net.IP{127, 0, 0, 1} + ipStr := ip.String() - const dirname = "dir" - 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), - }, - } + testFS := fstest.MapFS{"dir/file1": &fstest.MapFile{Data: []byte(ipStr + ` hostname` + nl)}} // event is a convenient alias for an empty struct{} to emit test events. type event = struct{} @@ -157,119 +147,117 @@ func TestHostsContainer_Refresh(t *testing.T) { w := &aghtest.FSWatcher{ OnEvents: func() (e <-chan event) { return eventsCh }, OnAdd: func(name string) (err error) { - assert.Equal(t, dirname, name) + assert.Equal(t, "dir", name) return nil }, 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) - checkRefresh := func(t *testing.T, wantHosts *stringutil.Set) { + checkRefresh := func(t *testing.T, wantHosts Hosts) { upd, ok := <-hc.Upd() require.True(t, ok) require.NotNil(t, upd) assert.Equal(t, 1, upd.Len()) - v, ok := upd.Get(knownIP) + v, ok := upd.Get(ip) require.True(t, ok) - var hosts *stringutil.Set - hosts, ok = v.(*stringutil.Set) + var hosts *Hosts + hosts, ok = v.(*Hosts) 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) { - 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) { - 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) { - assert.Empty(t, hc.Upd()) + // 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) + + // 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) { - 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{ - fp1: &fstest.MapFile{Data: []byte{1}}, - fp2: &fstest.MapFile{Data: []byte{2}}, - fp3: &fstest.MapFile{Data: []byte{3}}, + "dir_0/file_1": &fstest.MapFile{Data: []byte{1}}, + "dir_0/file_2": &fstest.MapFile{Data: []byte{2}}, + "dir_0/dir_1/file_3": &fstest.MapFile{Data: []byte{3}}, } testCases := []struct { - name string - wantErr error - want []string - paths []string + name string + paths []string + want []string }{{ - name: "no_paths", - wantErr: nil, - want: nil, - paths: nil, + name: "no_paths", + paths: nil, + want: nil, }, { - name: "single_file", - wantErr: nil, - want: []string{fp1}, - paths: []string{fp1}, + name: "single_file", + paths: []string{"dir_0/file_1"}, + want: []string{"dir_0/file_1"}, }, { - name: "several_files", - wantErr: nil, - want: []string{fp1, fp2}, - paths: []string{fp1, fp2}, + name: "several_files", + paths: []string{"dir_0/file_1", "dir_0/file_2"}, + want: []string{"dir_0/file_1", "dir_0/file_2"}, }, { - name: "whole_dir", - wantErr: nil, - want: []string{path.Join(dir0, "*")}, - paths: []string{dir0}, + name: "whole_dir", + paths: []string{"dir_0"}, + want: []string{"dir_0/*"}, }, { - name: "file_and_dir", - wantErr: nil, - want: []string{fp1, path.Join(dir0, dir1, "*")}, - paths: []string{fp1, path.Join(dir0, dir1)}, + name: "file_and_dir", + paths: []string{"dir_0/file_1", "dir_0/dir_1"}, + want: []string{"dir_0/file_1", "dir_0/dir_1/*"}, }, { - name: "non-existing", - wantErr: nil, - want: nil, - paths: []string{path.Join(dir0, "file_3")}, + name: "non-existing", + paths: []string{path.Join("dir_0", "file_3")}, + want: nil, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { patterns, err := pathsToPatterns(gsfs, tc.paths) - if tc.wantErr != nil { - assert.ErrorIs(t, err, tc.wantErr) - - return - } - require.NoError(t, err) 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) { const listID = 1234 testdata := os.DirFS("./testdata") nRewrites := func(t *testing.T, res *urlfilter.DNSResult, n int) (rws []*rules.DNSRewrite) { - t.Helper() - rewrites := res.DNSRewrites() - assert.Len(t, rewrites, n) + require.Len(t, rewrites, n) for _, rewrite := range rewrites { require.Equal(t, listID, rewrite.FilterListID) @@ -346,6 +392,15 @@ func TestHostsContainer(t *testing.T) { testTail: func(t *testing.T, res *urlfilter.DNSResult) { 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", req: urlfilter.DNSRequest{ @@ -419,12 +474,8 @@ func TestHostsContainer(t *testing.T) { } func TestUniqueRules_ParseLine(t *testing.T) { - const ( - hostname = "localhost" - alias = "hocallost" - ) - - knownIP := net.IP{127, 0, 0, 1} + ip := net.IP{127, 0, 0, 1} + ipStr := ip.String() testCases := []struct { name string @@ -433,39 +484,39 @@ func TestUniqueRules_ParseLine(t *testing.T) { wantHosts []string }{{ name: "simple", - line: strings.Join([]string{knownIP.String(), hostname}, sp), - wantIP: knownIP, - wantHosts: []string{"localhost"}, + line: ipStr + ` hostname`, + wantIP: ip, + wantHosts: []string{"hostname"}, }, { name: "aliases", - line: strings.Join([]string{knownIP.String(), hostname, alias}, sp), - wantIP: knownIP, - wantHosts: []string{"localhost", "hocallost"}, + line: ipStr + ` hostname alias`, + wantIP: ip, + wantHosts: []string{"hostname", "alias"}, }, { name: "invalid_line", - line: knownIP.String(), + line: ipStr, wantIP: nil, wantHosts: nil, }, { name: "invalid_line_hostname", - line: strings.Join([]string{knownIP.String(), "#" + hostname}, sp), - wantIP: knownIP, + line: ipStr + ` # hostname`, + wantIP: ip, wantHosts: nil, }, { name: "commented_aliases", - line: strings.Join([]string{knownIP.String(), hostname, "#" + alias}, sp), - wantIP: knownIP, - wantHosts: []string{"localhost"}, + line: ipStr + ` hostname # alias`, + wantIP: ip, + wantHosts: []string{"hostname"}, }, { name: "whole_comment", - line: strings.Join([]string{"#", knownIP.String(), hostname}, sp), + line: `# ` + ipStr + ` hostname`, wantIP: nil, wantHosts: nil, }, { name: "partial_comment", - line: strings.Join([]string{knownIP.String(), hostname[:4] + "#" + hostname[4:]}, sp), - wantIP: knownIP, - wantHosts: []string{hostname[:4]}, + line: ipStr + ` host#name`, + wantIP: ip, + wantHosts: []string{"host"}, }, { name: "empty", line: ``, @@ -476,8 +527,8 @@ func TestUniqueRules_ParseLine(t *testing.T) { for _, tc := range testCases { hp := hostsParser{} t.Run(tc.name, func(t *testing.T) { - ip, hosts := hp.parseLine(tc.line) - assert.True(t, tc.wantIP.Equal(ip)) + got, hosts := hp.parseLine(tc.line) + assert.True(t, tc.wantIP.Equal(got)) assert.Equal(t, tc.wantHosts, hosts) }) } diff --git a/internal/aghnet/testdata/etc_hosts b/internal/aghnet/testdata/etc_hosts index 15e07566..afe7011c 100644 --- a/internal/aghnet/testdata/etc_hosts +++ b/internal/aghnet/testdata/etc_hosts @@ -12,9 +12,19 @@ 1.0.0.3 * 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. ::1 simplehost :: hello hello.world ::2 a.whole lot.of aliases for.testing ::3 * ::4 *.com +:: hello.world.again +::1 simplehost +:: hello.world \ No newline at end of file diff --git a/internal/home/clients.go b/internal/home/clients.go index 5643f6b0..94dd41f7 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -783,12 +783,17 @@ func (clients *clientsContainer) addFromHostsFile(hosts *netutil.IPMap) { n := 0 hosts.Range(func(ip net.IP, v interface{}) (cont bool) { - names, ok := v.(*stringutil.Set) + hosts, ok := v.(*aghnet.Hosts) if !ok { 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) { n++ }