From 2e8352d31c8e4f85ed45f7cbc09e8106da046db3 Mon Sep 17 00:00:00 2001 From: Ainar Garipov Date: Thu, 17 Dec 2020 13:32:46 +0300 Subject: [PATCH] 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 Date: Thu Dec 17 13:12:27 2020 +0300 querylog: remove pre-v0.99.3 compatibility code commit 2af0ee43c2444a7d842fcff057f2ba02f300244b Author: Ainar Garipov Date: Thu Dec 17 13:00:27 2020 +0300 all: improve documentation commit 3add300a42f0aa67bb315a448e294636c85d0b3b Author: Ainar Garipov Date: Wed Dec 16 18:30:01 2020 +0300 all: improve changelog commit e04ef701fc2de7f4453729e617641c47e0883679 Author: Ainar Garipov Date: Wed Dec 16 17:56:53 2020 +0300 all: improve code and documentation commit 4f04845ae275ae4291869e00c62e4ff81b01eaa3 Author: Ainar Garipov Date: Wed Dec 16 17:01:08 2020 +0300 all: document changes, improve api commit bc59b7656a402d0c65f13bd74a71d8dda6a8a65d Author: Ainar Garipov Date: Tue Dec 15 18:22:01 2020 +0300 all: allow multiple rules in dns filter results --- AGHTechDoc.md | 22 +- CHANGELOG.md | 7 +- HACKING.md | 19 +- internal/dnsfilter/blocked.go | 8 +- internal/dnsfilter/dnsfilter.go | 182 ++++++++++------ internal/dnsfilter/dnsfilter_test.go | 202 +++++++++--------- internal/dnsfilter/rewrites.go | 10 +- internal/dnsfilter/rewrites_test.go | 10 +- .../dnsfilter/{sbpc.go => safebrowsing.go} | 32 +-- .../{sbpc_test.go => safebrowsing_test.go} | 0 internal/dnsfilter/safesearch.go | 45 ++-- internal/dnsforward/dnsforward.go | 4 +- internal/dnsforward/dnsforward_test.go | 8 +- internal/dnsforward/filter.go | 6 +- internal/dnsforward/msg.go | 11 +- internal/home/controlfiltering.go | 31 ++- internal/home/home.go | 2 +- internal/querylog/decode.go | 153 ++++++++----- internal/querylog/decode_test.go | 26 +-- internal/querylog/json.go | 70 +++--- internal/querylog/qlog_test.go | 6 +- internal/querylog/searchcriteria.go | 4 +- internal/tools/go.mod | 2 +- internal/tools/go.sum | 3 + openapi/CHANGELOG.md | 32 +++ openapi/openapi.yaml | 55 ++++- scripts/go-lint.sh | 2 +- staticcheck.conf | 14 ++ 28 files changed, 610 insertions(+), 356 deletions(-) rename internal/dnsfilter/{sbpc.go => safebrowsing.go} (91%) rename internal/dnsfilter/{sbpc_test.go => safebrowsing_test.go} (100%) create mode 100644 staticcheck.conf diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 45dee068..b91b8586 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -1833,16 +1833,22 @@ Response: 200 OK { - "reason":"FilteredBlackList", - "filter_id":1, - "rule":"||doubleclick.net^", - "service_name": "...", // set if reason=FilteredBlockedService - - // if reason=ReasonRewrite: - "cname": "...", - "ip_addrs": ["1.2.3.4", ...], + "reason":"FilteredBlackList", + "rules":{ + "filter_list_id":42, + "text":"||doubleclick.net^", + }, + // If we have "reason":"FilteredBlockedService". + "service_name": "...", + // If we have "reason":"Rewrite". + "cname": "...", + "ip_addrs": ["1.2.3.4", ...] } +There are also deprecated properties `filter_id` and `rule` on the top level of +the response object. Their usaga should be replaced with +`rules[*].filter_list_id` and `rules[*].text` correspondingly. See the +_OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file. ## Log-in page diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ccb70e4..fda4fe42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ and this project adheres to ### Added -- Detecting of network interface configurated to have static IP address via +- The host checking API and the query logs API can now return multiple matched + rules ([#2102]). +- Detecting of network interface configured to have static IP address via `/etc/network/interfaces` ([#2302]). - DNSCrypt protocol support ([#1361]). - A 5 second wait period until a DHCP server's network interface gets an IP @@ -24,6 +26,7 @@ and this project adheres to - HTTP API request body size limit ([#2305]). [#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361 +[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102 [#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302 [#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304 [#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305 @@ -64,7 +67,9 @@ and this project adheres to [#2345]: https://github.com/AdguardTeam/AdGuardHome/issues/2345 [#2355]: https://github.com/AdguardTeam/AdGuardHome/issues/2355 +### Removed +- Support for pre-v0.99.3 format of query logs ([#2102]). ## [v0.104.3] - 2020-11-19 diff --git a/HACKING.md b/HACKING.md index fdc2e3a9..ae14b784 100644 --- a/HACKING.md +++ b/HACKING.md @@ -78,6 +78,14 @@ The rules are mostly sorted in the alphabetical order. * Prefer constants to variables where possible. Reduce global variables. Use [constant errors] instead of `errors.New`. + * Unused arguments in anonymous functions must be called `_`: + + ```go + v.onSuccess = func(_ int, msg string) { + // … + } + ``` + * Use linters. * Use named returns to improve readability of function signatures. @@ -106,7 +114,16 @@ The rules are mostly sorted in the alphabetical order. ```go // Foo implements the Fooer interface for *foo. func (f *foo) Foo() { - // … + // … + } + ``` + + When the implemented interface is unexported: + + ```go + // Unwrap implements the hidden wrapper interface for *fooError. + func (err *fooError) Unwrap() (unwrapped error) { + // … } ``` diff --git a/internal/dnsfilter/blocked.go b/internal/dnsfilter/blocked.go index 08990e0a..48b02932 100644 --- a/internal/dnsfilter/blocked.go +++ b/internal/dnsfilter/blocked.go @@ -188,7 +188,7 @@ func BlockedSvcKnown(s string) bool { } // ApplyBlockedServices - set blocked services settings for this DNS request -func (d *Dnsfilter) ApplyBlockedServices(setts *RequestFilteringSettings, list []string, global bool) { +func (d *DNSFilter) ApplyBlockedServices(setts *RequestFilteringSettings, list []string, global bool) { setts.ServicesRules = []ServiceEntry{} if global { d.confLock.RLock() @@ -210,7 +210,7 @@ func (d *Dnsfilter) ApplyBlockedServices(setts *RequestFilteringSettings, list [ } } -func (d *Dnsfilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { d.confLock.RLock() list := d.Config.BlockedServices d.confLock.RUnlock() @@ -223,7 +223,7 @@ func (d *Dnsfilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req } } -func (d *Dnsfilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) { list := []string{} err := json.NewDecoder(r.Body).Decode(&list) if err != nil { @@ -241,7 +241,7 @@ func (d *Dnsfilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ } // registerBlockedServicesHandlers - register HTTP handlers -func (d *Dnsfilter) registerBlockedServicesHandlers() { +func (d *DNSFilter) registerBlockedServicesHandlers() { d.Config.HTTPRegister("GET", "/control/blocked_services/list", d.handleBlockedServicesList) d.Config.HTTPRegister("POST", "/control/blocked_services/set", d.handleBlockedServicesSet) } diff --git a/internal/dnsfilter/dnsfilter.go b/internal/dnsfilter/dnsfilter.go index 2690507d..1735154d 100644 --- a/internal/dnsfilter/dnsfilter.go +++ b/internal/dnsfilter/dnsfilter.go @@ -91,8 +91,8 @@ type filtersInitializerParams struct { blockFilters []Filter } -// Dnsfilter holds added rules and performs hostname matches against the rules -type Dnsfilter struct { +// DNSFilter matches hostnames and DNS requests against filtering rules. +type DNSFilter struct { rulesStorage *filterlist.RuleStorage filteringEngine *urlfilter.DNSEngine rulesStorageWhite *filterlist.RuleStorage @@ -129,7 +129,7 @@ const ( NotFilteredNotFound Reason = iota // NotFilteredWhiteList - the host is explicitly whitelisted NotFilteredWhiteList - // NotFilteredError is return where there was an error during + // NotFilteredError is returned when there was an error during // checking. Reserved, currently unused. NotFilteredError @@ -148,27 +148,32 @@ const ( // FilteredBlockedService - the host is blocked by "blocked services" settings FilteredBlockedService - // ReasonRewrite - rewrite rule was applied + // ReasonRewrite is returned when there was a rewrite by + // a legacy DNS Rewrite rule. ReasonRewrite - // RewriteEtcHosts - rewrite by /etc/hosts rule - RewriteEtcHosts + // RewriteAutoHosts is returned when there was a rewrite by + // autohosts rules (/etc/hosts and so on). + RewriteAutoHosts ) +// TODO(a.garipov): Resync with actual code names or replace completely +// in HTTP API v1. var reasonNames = []string{ - "NotFilteredNotFound", - "NotFilteredWhiteList", - "NotFilteredError", + NotFilteredNotFound: "NotFilteredNotFound", + NotFilteredWhiteList: "NotFilteredWhiteList", + NotFilteredError: "NotFilteredError", - "FilteredBlackList", - "FilteredSafeBrowsing", - "FilteredParental", - "FilteredInvalid", - "FilteredSafeSearch", - "FilteredBlockedService", + FilteredBlackList: "FilteredBlackList", + FilteredSafeBrowsing: "FilteredSafeBrowsing", + FilteredParental: "FilteredParental", + FilteredInvalid: "FilteredInvalid", + FilteredSafeSearch: "FilteredSafeSearch", + FilteredBlockedService: "FilteredBlockedService", - "Rewrite", - "RewriteEtcHosts", + ReasonRewrite: "Rewrite", + + RewriteAutoHosts: "RewriteEtcHosts", } func (r Reason) String() string { @@ -189,7 +194,7 @@ func (r Reason) In(reasons ...Reason) bool { } // GetConfig - get configuration -func (d *Dnsfilter) GetConfig() RequestFilteringSettings { +func (d *DNSFilter) GetConfig() RequestFilteringSettings { c := RequestFilteringSettings{} // d.confLock.RLock() c.SafeSearchEnabled = d.Config.SafeSearchEnabled @@ -200,7 +205,7 @@ func (d *Dnsfilter) GetConfig() RequestFilteringSettings { } // WriteDiskConfig - write configuration -func (d *Dnsfilter) WriteDiskConfig(c *Config) { +func (d *DNSFilter) WriteDiskConfig(c *Config) { d.confLock.Lock() *c = d.Config c.Rewrites = rewriteArrayDup(d.Config.Rewrites) @@ -211,7 +216,7 @@ func (d *Dnsfilter) WriteDiskConfig(c *Config) { // SetFilters - set new filters (synchronously or asynchronously) // When filters are set asynchronously, the old filters continue working until the new filters are ready. // In this case the caller must ensure that the old filter files are intact. -func (d *Dnsfilter) SetFilters(blockFilters, allowFilters []Filter, async bool) error { +func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool) error { if async { params := filtersInitializerParams{ allowFilters: allowFilters, @@ -245,7 +250,7 @@ func (d *Dnsfilter) SetFilters(blockFilters, allowFilters []Filter, async bool) } // Starts initializing new filters by signal from channel -func (d *Dnsfilter) filtersInitializer() { +func (d *DNSFilter) filtersInitializer() { for { params := <-d.filtersInitializerChan err := d.initFiltering(params.allowFilters, params.blockFilters) @@ -257,13 +262,13 @@ func (d *Dnsfilter) filtersInitializer() { } // Close - close the object -func (d *Dnsfilter) Close() { +func (d *DNSFilter) Close() { d.engineLock.Lock() defer d.engineLock.Unlock() d.reset() } -func (d *Dnsfilter) reset() { +func (d *DNSFilter) reset() { var err error if d.rulesStorage != nil { @@ -290,34 +295,60 @@ type dnsFilterContext struct { var gctx dnsFilterContext // global dnsfilter context -// Result holds state of hostname check -type Result struct { - IsFiltered bool `json:",omitempty"` // True if the host name is filtered - Reason Reason `json:",omitempty"` // Reason for blocking / unblocking - Rule string `json:",omitempty"` // Original rule text - IP net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax - FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to - - // for ReasonRewrite: - CanonName string `json:",omitempty"` // CNAME value - - // for RewriteEtcHosts: - ReverseHosts []string `json:",omitempty"` - - // for ReasonRewrite & RewriteEtcHosts: - IPList []net.IP `json:",omitempty"` // list of IP addresses - - // for FilteredBlockedService: - ServiceName string `json:",omitempty"` // Name of the blocked service +// ResultRule contains information about applied rules. +type ResultRule struct { + // FilterListID is the ID of the rule's filter list. + FilterListID int64 `json:",omitempty"` + // Text is the text of the rule. + Text string `json:",omitempty"` + // IP is the host IP. It is nil unless the rule uses the + // /etc/hosts syntax or the reason is FilteredSafeSearch. + IP net.IP `json:",omitempty"` } -// Matched can be used to see if any match at all was found, no matter filtered or not +// Result contains the result of a request check. +// +// All fields transitively have omitempty tags so that the query log +// doesn't become too large. +// +// TODO(a.garipov): Clarify relationships between fields. Perhaps +// replace with a sum type or an interface? +type Result struct { + // IsFiltered is true if the request is filtered. + IsFiltered bool `json:",omitempty"` + + // Reason is the reason for blocking or unblocking the request. + Reason Reason `json:",omitempty"` + + // Rules are applied rules. If Rules are not empty, each rule + // is not nil. + Rules []*ResultRule `json:",omitempty"` + + // ReverseHosts is the reverse lookup rewrite result. It is + // empty unless Reason is set to RewriteAutoHosts. + ReverseHosts []string `json:",omitempty"` + + // IPList is the lookup rewrite result. It is empty unless + // Reason is set to RewriteAutoHosts or ReasonRewrite. + IPList []net.IP `json:",omitempty"` + + // CanonName is the CNAME value from the lookup rewrite result. + // It is empty unless Reason is set to ReasonRewrite. + CanonName string `json:",omitempty"` + + // ServiceName is the name of the blocked service. It is empty + // unless Reason is set to FilteredBlockedService. + ServiceName string `json:",omitempty"` +} + +// Matched returns true if any match at all was found regardless of +// whether it was filtered or not. func (r Reason) Matched() bool { return r != NotFilteredNotFound } -// CheckHostRules tries to match the host against filtering rules only -func (d *Dnsfilter) CheckHostRules(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { +// CheckHostRules tries to match the host against filtering rules only. +func (d *DNSFilter) CheckHostRules(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { if !setts.FilteringEnabled { return Result{}, nil } @@ -325,9 +356,9 @@ func (d *Dnsfilter) CheckHostRules(host string, qtype uint16, setts *RequestFilt return d.matchHost(host, qtype, *setts) } -// CheckHost tries to match the host against filtering rules, -// then safebrowsing and parental if they are enabled -func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { +// CheckHost tries to match the host against filtering rules, then +// safebrowsing and parental control rules, if they are enabled. +func (d *DNSFilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { // sometimes DNS clients will try to resolve ".", which is a request to get root servers if host == "" { return Result{Reason: NotFilteredNotFound}, nil @@ -413,10 +444,10 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering return Result{}, nil } -func (d *Dnsfilter) checkAutoHosts(host string, qtype uint16, result *Result) (matched bool) { +func (d *DNSFilter) checkAutoHosts(host string, qtype uint16, result *Result) (matched bool) { ips := d.Config.AutoHosts.Process(host, qtype) if ips != nil { - result.Reason = RewriteEtcHosts + result.Reason = RewriteAutoHosts result.IPList = ips return true @@ -424,7 +455,7 @@ func (d *Dnsfilter) checkAutoHosts(host string, qtype uint16, result *Result) (m revHosts := d.Config.AutoHosts.ProcessReverse(host, qtype) if len(revHosts) != 0 { - result.Reason = RewriteEtcHosts + result.Reason = RewriteAutoHosts // TODO(a.garipov): Optimize this with a buffer. result.ReverseHosts = make([]string, len(revHosts)) @@ -445,7 +476,7 @@ func (d *Dnsfilter) checkAutoHosts(host string, qtype uint16, result *Result) (m // . repeat for the new domain name (Note: we return only the last CNAME) // . Find A or AAAA record for a domain name (exact match or by wildcard) // . if found, set IP addresses (IPv4 or IPv6 depending on qtype) in Result.IPList array -func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result { +func (d *DNSFilter) processRewrites(host string, qtype uint16) Result { var res Result d.confLock.RLock() @@ -504,9 +535,16 @@ func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result { res.Reason = FilteredBlockedService res.IsFiltered = true res.ServiceName = s.Name - res.Rule = rule.Text() - log.Debug("Blocked Services: matched rule: %s host: %s service: %s", - res.Rule, host, s.Name) + + ruleText := rule.Text() + res.Rules = []*ResultRule{{ + FilterListID: int64(rule.GetFilterListID()), + Text: ruleText, + }} + + log.Debug("blocked services: matched rule: %s host: %s service: %s", + ruleText, host, s.Name) + return res } } @@ -573,7 +611,7 @@ func createFilteringEngine(filters []Filter) (*filterlist.RuleStorage, *urlfilte } // Initialize urlfilter objects. -func (d *Dnsfilter) initFiltering(allowFilters, blockFilters []Filter) error { +func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error { rulesStorage, filteringEngine, err := createFilteringEngine(blockFilters) if err != nil { return err @@ -600,7 +638,7 @@ func (d *Dnsfilter) initFiltering(allowFilters, blockFilters []Filter) error { // matchHost is a low-level way to check only if hostname is filtered by rules, // skipping expensive safebrowsing and parental lookups. -func (d *Dnsfilter) matchHost(host string, qtype uint16, setts RequestFilteringSettings) (Result, error) { +func (d *DNSFilter) matchHost(host string, qtype uint16, setts RequestFilteringSettings) (Result, error) { d.engineLock.RLock() // Keep in mind that this lock must be held no just when calling Match() // but also while using the rules returned by it. @@ -658,7 +696,8 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16, setts RequestFilteringS log.Debug("Filtering: found rule for host %q: %q list_id: %d", host, rule.Text(), rule.GetFilterListID()) res := makeResult(rule, FilteredBlackList) - res.IP = rule.IP.To4() + res.Rules[0].IP = rule.IP.To4() + return res, nil } @@ -667,7 +706,8 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16, setts RequestFilteringS log.Debug("Filtering: found rule for host %q: %q list_id: %d", host, rule.Text(), rule.GetFilterListID()) res := makeResult(rule, FilteredBlackList) - res.IP = rule.IP + res.Rules[0].IP = rule.IP + return res, nil } @@ -683,22 +723,28 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16, setts RequestFilteringS log.Debug("Filtering: found rule for host %q: %q list_id: %d", host, rule.Text(), rule.GetFilterListID()) res := makeResult(rule, FilteredBlackList) - res.IP = net.IP{} + res.Rules[0].IP = net.IP{} + return res, nil } return Result{}, nil } -// Construct Result object +// makeResult returns a properly constructed Result. func makeResult(rule rules.Rule, reason Reason) Result { - res := Result{} - res.FilterID = int64(rule.GetFilterListID()) - res.Rule = rule.Text() - res.Reason = reason + res := Result{ + Reason: reason, + Rules: []*ResultRule{{ + FilterListID: int64(rule.GetFilterListID()), + Text: rule.Text(), + }}, + } + if reason == FilteredBlackList { res.IsFiltered = true } + return res } @@ -708,7 +754,7 @@ func InitModule() { } // New creates properly initialized DNS Filter that is ready to be used. -func New(c *Config, blockFilters []Filter) *Dnsfilter { +func New(c *Config, blockFilters []Filter) *DNSFilter { if c != nil { cacheConf := cache.Config{ EnableLRU: true, @@ -730,7 +776,7 @@ func New(c *Config, blockFilters []Filter) *Dnsfilter { } } - d := new(Dnsfilter) + d := new(DNSFilter) err := d.initSecurityServices() if err != nil { @@ -768,7 +814,7 @@ func New(c *Config, blockFilters []Filter) *Dnsfilter { // Start - start the module: // . start async filtering initializer goroutine // . register web handlers -func (d *Dnsfilter) Start() { +func (d *DNSFilter) Start() { d.filtersInitializerChan = make(chan filtersInitializerParams, 1) go d.filtersInitializer() diff --git a/internal/dnsfilter/dnsfilter_test.go b/internal/dnsfilter/dnsfilter_test.go index 1eb7a31c..96376162 100644 --- a/internal/dnsfilter/dnsfilter_test.go +++ b/internal/dnsfilter/dnsfilter_test.go @@ -41,7 +41,7 @@ func purgeCaches() { } } -func NewForTest(c *Config, filters []Filter) *Dnsfilter { +func NewForTest(c *Config, filters []Filter) *DNSFilter { setts = RequestFilteringSettings{} setts.FilteringEnabled = true if c != nil { @@ -58,38 +58,48 @@ func NewForTest(c *Config, filters []Filter) *Dnsfilter { return d } -func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) { +func (d *DNSFilter) checkMatch(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + if !res.IsFiltered { t.Errorf("Expected hostname %s to match", hostname) } } -func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) { +func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) { t.Helper() - ret, err := d.CheckHost(hostname, qtype, &setts) + + res, err := d.CheckHost(hostname, qtype, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + + if !res.IsFiltered { t.Errorf("Expected hostname %s to match", hostname) } - if ret.IP == nil || ret.IP.String() != ip { - t.Errorf("Expected ip %s to match, actual: %v", ip, ret.IP) + + if len(res.Rules) == 0 { + t.Errorf("Expected result to have rules") + + return + } + + r := res.Rules[0] + if r.IP == nil || r.IP.String() != ip { + t.Errorf("Expected ip %s to match, actual: %v", ip, r.IP) } } -func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) { +func (d *DNSFilter) checkMatchEmpty(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } - if ret.IsFiltered { + if res.IsFiltered { t.Errorf("Expected hostname %s to not match", hostname) } } @@ -120,26 +130,43 @@ func TestEtcHostsMatching(t *testing.T) { d.checkMatchIP(t, "block.com", "0.0.0.0", dns.TypeA) // ...but empty IPv6 - ret, err := d.CheckHost("block.com", dns.TypeAAAA, &setts) - assert.True(t, err == nil && ret.IsFiltered && ret.IP != nil && len(ret.IP) == 0) - assert.True(t, ret.Rule == "0.0.0.0 block.com") + res, err := d.CheckHost("block.com", dns.TypeAAAA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, "0.0.0.0 block.com", res.Rules[0].Text) + assert.Len(t, res.Rules[0].IP, 0) + } // IPv6 d.checkMatchIP(t, "ipv6.com", addr6, dns.TypeAAAA) // ...but empty IPv4 - ret, err = d.CheckHost("ipv6.com", dns.TypeA, &setts) - assert.True(t, err == nil && ret.IsFiltered && ret.IP != nil && len(ret.IP) == 0) + res, err = d.CheckHost("ipv6.com", dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, "::1 ipv6.com", res.Rules[0].Text) + assert.Len(t, res.Rules[0].IP, 0) + } // 2 IPv4 (return only the first one) - ret, err = d.CheckHost("host2", dns.TypeA, &setts) - assert.True(t, err == nil && ret.IsFiltered) - assert.True(t, ret.IP != nil && ret.IP.Equal(net.ParseIP("0.0.0.1"))) + res, err = d.CheckHost("host2", dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + loopback4 := net.IP{0, 0, 0, 1} + assert.Equal(t, res.Rules[0].IP, loopback4) + } // ...and 1 IPv6 address - ret, err = d.CheckHost("host2", dns.TypeAAAA, &setts) - assert.True(t, err == nil && ret.IsFiltered) - assert.True(t, ret.IP != nil && ret.IP.Equal(net.ParseIP("::1"))) + res, err = d.CheckHost("host2", dns.TypeAAAA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + loopback6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + assert.Equal(t, res.Rules[0].IP, loopback6) + } } // SAFE BROWSING @@ -206,13 +233,11 @@ func TestCheckHostSafeSearchYandex(t *testing.T) { // Check host for each domain for _, host := range yandex { - result, err := d.CheckHost(host, dns.TypeA, &setts) - if err != nil { - t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err) - } - - if result.IP.String() != "213.180.193.56" { - t.Errorf("SafeSearch doesn't work for yandex domain `%s`", host) + res, err := d.CheckHost(host, dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, res.Rules[0].IP.String(), "213.180.193.56") } } } @@ -226,13 +251,11 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) { // Check host for each domain for _, host := range googleDomains { - result, err := d.CheckHost(host, dns.TypeA, &setts) - if err != nil { - t.Errorf("SafeSearch doesn't work for %s cause %s", host, err) - } - - if result.IP == nil { - t.Errorf("SafeSearch doesn't work for %s", host) + res, err := d.CheckHost(host, dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.NotEqual(t, res.Rules[0].IP.String(), "0.0.0.0") } } } @@ -242,40 +265,30 @@ func TestSafeSearchCacheYandex(t *testing.T) { defer d.Close() domain := "yandex.ru" - var result Result - var err error - - // Check host with disabled safesearch - result, err = d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("Cannot check host due to %s", err) - } - if result.IP != nil { - t.Fatalf("SafeSearch is not enabled but there is an answer for `%s` !", domain) - } + // Check host with disabled safesearch. + res, err := d.CheckHost(domain, dns.TypeA, &setts) + assert.Nil(t, err) + assert.False(t, res.IsFiltered) + assert.Len(t, res.Rules, 0) d = NewForTest(&Config{SafeSearchEnabled: true}, nil) defer d.Close() - result, err = d.CheckHost(domain, dns.TypeA, &setts) + res, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) } - // Fir yandex we already know valid ip - if result.IP.String() != "213.180.193.56" { - t.Fatalf("Wrong IP for %s safesearch: %s", domain, result.IP.String()) + // For yandex we already know valid ip. + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, res.Rules[0].IP.String(), "213.180.193.56") } - // Check cache + // Check cache. cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain) - - if !isFound { - t.Fatalf("Safesearch cache doesn't work for %s!", domain) - } - - if cachedValue.IP.String() != "213.180.193.56" { - t.Fatalf("Wrong IP in cache for %s safesearch: %s", domain, cachedValue.IP.String()) + assert.True(t, isFound) + if assert.Len(t, cachedValue.Rules, 1) { + assert.Equal(t, cachedValue.Rules[0].IP.String(), "213.180.193.56") } } @@ -283,13 +296,10 @@ func TestSafeSearchCacheGoogle(t *testing.T) { d := NewForTest(nil, nil) defer d.Close() domain := "www.google.ru" - result, err := d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("Cannot check host due to %s", err) - } - if result.IP != nil { - t.Fatalf("SafeSearch is not enabled but there is an answer!") - } + res, err := d.CheckHost(domain, dns.TypeA, &setts) + assert.Nil(t, err) + assert.False(t, res.IsFiltered) + assert.Len(t, res.Rules, 0) d = NewForTest(&Config{SafeSearchEnabled: true}, nil) defer d.Close() @@ -313,25 +323,17 @@ func TestSafeSearchCacheGoogle(t *testing.T) { } } - result, err = d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) + res, err = d.CheckHost(domain, dns.TypeA, &setts) + assert.Nil(t, err) + if assert.Len(t, res.Rules, 1) { + assert.True(t, res.Rules[0].IP.Equal(ip)) } - if result.IP.String() != ip.String() { - t.Fatalf("Wrong IP for %s safesearch: %s. Should be: %s", - domain, result.IP.String(), ip) - } - - // Check cache + // Check cache. cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain) - - if !isFound { - t.Fatalf("Safesearch cache doesn't work for %s!", domain) - } - - if cachedValue.IP.String() != ip.String() { - t.Fatalf("Wrong IP in cache for %s safesearch: %s", domain, cachedValue.IP.String()) + assert.True(t, isFound) + if assert.Len(t, cachedValue.Rules, 1) { + assert.True(t, cachedValue.Rules[0].IP.Equal(ip)) } } @@ -433,15 +435,15 @@ func TestMatching(t *testing.T) { d := NewForTest(nil, filters) defer d.Close() - ret, err := d.CheckHost(test.hostname, test.dnsType, &setts) + res, err := d.CheckHost(test.hostname, test.dnsType, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", test.hostname, err) } - if ret.IsFiltered != test.isFiltered { - t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret.IsFiltered, test.isFiltered) + if res.IsFiltered != test.isFiltered { + t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, res.IsFiltered, test.isFiltered) } - if ret.Reason != test.reason { - t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, ret.Reason.String(), test.reason.String()) + if res.Reason != test.reason { + t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, res.Reason.String(), test.reason.String()) } }) } @@ -466,16 +468,20 @@ func TestWhitelist(t *testing.T) { defer d.Close() // matched by white filter - ret, err := d.CheckHost("host1", dns.TypeA, &setts) + res, err := d.CheckHost("host1", dns.TypeA, &setts) assert.True(t, err == nil) - assert.True(t, !ret.IsFiltered && ret.Reason == NotFilteredWhiteList) - assert.True(t, ret.Rule == "||host1^") + assert.True(t, !res.IsFiltered && res.Reason == NotFilteredWhiteList) + if assert.Len(t, res.Rules, 1) { + assert.True(t, res.Rules[0].Text == "||host1^") + } // not matched by white filter, but matched by block filter - ret, err = d.CheckHost("host2", dns.TypeA, &setts) + res, err = d.CheckHost("host2", dns.TypeA, &setts) assert.True(t, err == nil) - assert.True(t, ret.IsFiltered && ret.Reason == FilteredBlackList) - assert.True(t, ret.Rule == "||host2^") + assert.True(t, res.IsFiltered && res.Reason == FilteredBlackList) + if assert.Len(t, res.Rules, 1) { + assert.True(t, res.Rules[0].Text == "||host2^") + } } // CLIENT SETTINGS @@ -559,11 +565,11 @@ func BenchmarkSafeBrowsing(b *testing.B) { defer d.Close() for n := 0; n < b.N; n++ { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + if !res.IsFiltered { b.Errorf("Expected hostname %s to match", hostname) } } @@ -575,11 +581,11 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + if !res.IsFiltered { b.Errorf("Expected hostname %s to match", hostname) } } diff --git a/internal/dnsfilter/rewrites.go b/internal/dnsfilter/rewrites.go index 0092344e..8db1fd0b 100644 --- a/internal/dnsfilter/rewrites.go +++ b/internal/dnsfilter/rewrites.go @@ -95,7 +95,7 @@ func (r *RewriteEntry) prepare() { } } -func (d *Dnsfilter) prepareRewrites() { +func (d *DNSFilter) prepareRewrites() { for i := range d.Rewrites { d.Rewrites[i].prepare() } @@ -148,7 +148,7 @@ type rewriteEntryJSON struct { Answer string `json:"answer"` } -func (d *Dnsfilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { arr := []*rewriteEntryJSON{} d.confLock.Lock() @@ -169,7 +169,7 @@ func (d *Dnsfilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { } } -func (d *Dnsfilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { jsent := rewriteEntryJSON{} err := json.NewDecoder(r.Body).Decode(&jsent) if err != nil { @@ -191,7 +191,7 @@ func (d *Dnsfilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { d.Config.ConfigModified() } -func (d *Dnsfilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) { jsent := rewriteEntryJSON{} err := json.NewDecoder(r.Body).Decode(&jsent) if err != nil { @@ -218,7 +218,7 @@ func (d *Dnsfilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) d.Config.ConfigModified() } -func (d *Dnsfilter) registerRewritesHandlers() { +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) diff --git a/internal/dnsfilter/rewrites_test.go b/internal/dnsfilter/rewrites_test.go index 31b5cc0d..4304b6de 100644 --- a/internal/dnsfilter/rewrites_test.go +++ b/internal/dnsfilter/rewrites_test.go @@ -9,7 +9,7 @@ import ( ) func TestRewrites(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // CNAME, A, AAAA d.Rewrites = []RewriteEntry{ {"somecname", "somehost.com", 0, nil}, @@ -104,7 +104,7 @@ func TestRewrites(t *testing.T) { } func TestRewritesLevels(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // exact host, wildcard L2, wildcard L3 d.Rewrites = []RewriteEntry{ {"host.com", "1.1.1.1", 0, nil}, @@ -133,7 +133,7 @@ func TestRewritesLevels(t *testing.T) { } func TestRewritesExceptionCNAME(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // wildcard; exception for a sub-domain d.Rewrites = []RewriteEntry{ {"*.host.com", "2.2.2.2", 0, nil}, @@ -153,7 +153,7 @@ func TestRewritesExceptionCNAME(t *testing.T) { } func TestRewritesExceptionWC(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // wildcard; exception for a sub-wildcard d.Rewrites = []RewriteEntry{ {"*.host.com", "2.2.2.2", 0, nil}, @@ -173,7 +173,7 @@ func TestRewritesExceptionWC(t *testing.T) { } func TestRewritesExceptionIP(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // exception for AAAA record d.Rewrites = []RewriteEntry{ {"host.com", "1.2.3.4", 0, nil}, diff --git a/internal/dnsfilter/sbpc.go b/internal/dnsfilter/safebrowsing.go similarity index 91% rename from internal/dnsfilter/sbpc.go rename to internal/dnsfilter/safebrowsing.go index 29a39fa2..f5aaca9f 100644 --- a/internal/dnsfilter/sbpc.go +++ b/internal/dnsfilter/safebrowsing.go @@ -1,5 +1,3 @@ -// Safe Browsing, Parental Control - package dnsfilter import ( @@ -22,6 +20,8 @@ import ( "golang.org/x/net/publicsuffix" ) +// Safe browsing and parental control methods. + const ( dnsTimeout = 3 * time.Second defaultSafebrowsingServer = `https://dns-family.adguard.com/dns-query` @@ -30,7 +30,7 @@ const ( pcTXTSuffix = `pc.dns.adguard.com.` ) -func (d *Dnsfilter) initSecurityServices() error { +func (d *DNSFilter) initSecurityServices() error { var err error d.safeBrowsingServer = defaultSafebrowsingServer d.parentalServer = defaultParentalServer @@ -287,7 +287,7 @@ func check(c *sbCtx, r Result, u upstream.Upstream) (Result, error) { return Result{}, nil } -func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) { +func (d *DNSFilter) checkSafeBrowsing(host string) (Result, error) { if log.GetLevel() >= log.DEBUG { timer := log.StartTimer() defer timer.LogElapsed("SafeBrowsing lookup for %s", host) @@ -301,12 +301,14 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) { res := Result{ IsFiltered: true, Reason: FilteredSafeBrowsing, - Rule: "adguard-malware-shavar", + Rules: []*ResultRule{{ + Text: "adguard-malware-shavar", + }}, } return check(ctx, res, d.safeBrowsingUpstream) } -func (d *Dnsfilter) checkParental(host string) (Result, error) { +func (d *DNSFilter) checkParental(host string) (Result, error) { if log.GetLevel() >= log.DEBUG { timer := log.StartTimer() defer timer.LogElapsed("Parental lookup for %s", host) @@ -320,7 +322,9 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) { res := Result{ IsFiltered: true, Reason: FilteredParental, - Rule: "parental CATEGORY_BLACKLISTED", + Rules: []*ResultRule{{ + Text: "parental CATEGORY_BLACKLISTED", + }}, } return check(ctx, res, d.parentalUpstream) } @@ -331,17 +335,17 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -func (d *Dnsfilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) { d.Config.SafeBrowsingEnabled = true d.Config.ConfigModified() } -func (d *Dnsfilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) { d.Config.SafeBrowsingEnabled = false d.Config.ConfigModified() } -func (d *Dnsfilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "enabled": d.Config.SafeBrowsingEnabled, } @@ -358,17 +362,17 @@ func (d *Dnsfilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Requ } } -func (d *Dnsfilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) { d.Config.ParentalEnabled = true d.Config.ConfigModified() } -func (d *Dnsfilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) { d.Config.ParentalEnabled = false d.Config.ConfigModified() } -func (d *Dnsfilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "enabled": d.Config.ParentalEnabled, } @@ -386,7 +390,7 @@ func (d *Dnsfilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) } } -func (d *Dnsfilter) registerSecurityHandlers() { +func (d *DNSFilter) registerSecurityHandlers() { d.Config.HTTPRegister("POST", "/control/safebrowsing/enable", d.handleSafeBrowsingEnable) d.Config.HTTPRegister("POST", "/control/safebrowsing/disable", d.handleSafeBrowsingDisable) d.Config.HTTPRegister("GET", "/control/safebrowsing/status", d.handleSafeBrowsingStatus) diff --git a/internal/dnsfilter/sbpc_test.go b/internal/dnsfilter/safebrowsing_test.go similarity index 100% rename from internal/dnsfilter/sbpc_test.go rename to internal/dnsfilter/safebrowsing_test.go diff --git a/internal/dnsfilter/safesearch.go b/internal/dnsfilter/safesearch.go index 9485260b..4aefa5e1 100644 --- a/internal/dnsfilter/safesearch.go +++ b/internal/dnsfilter/safesearch.go @@ -18,7 +18,7 @@ import ( expire byte[4] res Result */ -func (d *Dnsfilter) setCacheResult(cache cache.Cache, host string, res Result) int { +func (d *DNSFilter) setCacheResult(cache cache.Cache, host string, res Result) int { var buf bytes.Buffer expire := uint(time.Now().Unix()) + d.Config.CacheTime*60 @@ -63,12 +63,12 @@ func getCachedResult(cache cache.Cache, host string) (Result, bool) { } // SafeSearchDomain returns replacement address for search engine -func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) { +func (d *DNSFilter) SafeSearchDomain(host string) (string, bool) { val, ok := safeSearchDomains[host] return val, ok } -func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) { +func (d *DNSFilter) checkSafeSearch(host string) (Result, error) { if log.GetLevel() >= log.DEBUG { timer := log.StartTimer() defer timer.LogElapsed("SafeSearch: lookup for %s", host) @@ -87,49 +87,52 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) { return Result{}, nil } - res := Result{IsFiltered: true, Reason: FilteredSafeSearch} + res := Result{ + IsFiltered: true, + Reason: FilteredSafeSearch, + Rules: []*ResultRule{{}}, + } + if ip := net.ParseIP(safeHost); ip != nil { - res.IP = ip + res.Rules[0].IP = ip valLen := d.setCacheResult(gctx.safeSearchCache, host, res) log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen) + return res, nil } // TODO this address should be resolved with upstream that was configured in dnsforward - addrs, err := net.LookupIP(safeHost) + ips, err := net.LookupIP(safeHost) if err != nil { log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err) return Result{}, err } - for _, i := range addrs { - if ipv4 := i.To4(); ipv4 != nil { - res.IP = ipv4 - break + for _, ip := range ips { + if ipv4 := ip.To4(); ipv4 != nil { + res.Rules[0].IP = ipv4 + + l := d.setCacheResult(gctx.safeSearchCache, host, res) + log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, l) + + return res, nil } } - if len(res.IP) == 0 { - return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost) - } - - // Cache result - valLen := d.setCacheResult(gctx.safeSearchCache, host, res) - log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen) - return res, nil + return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost) } -func (d *Dnsfilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) { d.Config.SafeSearchEnabled = true d.Config.ConfigModified() } -func (d *Dnsfilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) { d.Config.SafeSearchEnabled = false d.Config.ConfigModified() } -func (d *Dnsfilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "enabled": d.Config.SafeSearchEnabled, } diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index 3640101c..f1b7e7d2 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -48,7 +48,7 @@ var webRegistered bool // The zero Server is empty and ready for use. type Server struct { dnsProxy *proxy.Proxy // DNS proxy instance - dnsFilter *dnsfilter.Dnsfilter // DNS filter instance + dnsFilter *dnsfilter.DNSFilter // DNS filter instance dhcpServer dhcpd.ServerInterface // DHCP server instance (optional) queryLog querylog.QueryLog // Query log instance stats stats.Stats @@ -74,7 +74,7 @@ type Server struct { // DNSCreateParams - parameters for NewServer() type DNSCreateParams struct { - DNSFilter *dnsfilter.Dnsfilter + DNSFilter *dnsfilter.DNSFilter Stats stats.Stats QueryLog querylog.QueryLog DHCPServer dhcpd.ServerInterface diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go index 327fbe82..fdee8648 100644 --- a/internal/dnsforward/dnsforward_test.go +++ b/internal/dnsforward/dnsforward_test.go @@ -296,7 +296,7 @@ func TestBlockedRequest(t *testing.T) { func TestServerCustomClientUpstream(t *testing.T) { s := createTestServer(t) - s.conf.GetCustomUpstreamByClient = func(clientAddr string) *proxy.UpstreamConfig { + s.conf.GetCustomUpstreamByClient = func(_ string) *proxy.UpstreamConfig { uc := &proxy.UpstreamConfig{} u := &testUpstream{} u.ipv4 = map[string][]net.IP{} @@ -473,7 +473,7 @@ func TestBlockCNAME(t *testing.T) { func TestClientRulesForCNAMEMatching(t *testing.T) { s := createTestServer(t) testUpstm := &testUpstream{testCNAMEs, testIPv4, nil} - s.conf.FilterHandler = func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) { + s.conf.FilterHandler = func(_ string, settings *dnsfilter.RequestFilteringSettings) { settings.FilteringEnabled = false } err := s.startWithUpstream(testUpstm) @@ -863,6 +863,8 @@ func sendTestMessages(t *testing.T, conn *dns.Conn) { } func exchangeAndAssertResponse(t *testing.T, client *dns.Client, addr net.Addr, host, ip string) { + t.Helper() + req := createTestMessage(host) reply, _, err := client.Exchange(req, addr.String()) if err != nil { @@ -900,6 +902,8 @@ func assertGoogleAResponse(t *testing.T, reply *dns.Msg) { } func assertResponse(t *testing.T, reply *dns.Msg, ip string) { + t.Helper() + if len(reply.Answer) != 1 { t.Fatalf("DNS server returned reply with wrong number of answers - %d", len(reply.Answer)) } diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index 11267adf..83effc60 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -52,13 +52,13 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { // Return immediately if there's an error return nil, fmt.Errorf("dnsfilter failed to check host %q: %w", host, err) } else if res.IsFiltered { - log.Tracef("Host %s is filtered, reason - %q, matched rule: %q", host, res.Reason, res.Rule) + log.Tracef("Host %s is filtered, reason - %q, matched rule: %q", host, res.Reason, res.Rules[0].Text) d.Res = s.genDNSFilterMessage(d, &res) } else if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 && len(res.IPList) == 0 { ctx.origQuestion = d.Req.Question[0] // resolve canonical name, not the original host name d.Req.Question[0].Name = dns.Fqdn(res.CanonName) - } else if res.Reason == dnsfilter.RewriteEtcHosts && len(res.ReverseHosts) != 0 { + } else if res.Reason == dnsfilter.RewriteAutoHosts && len(res.ReverseHosts) != 0 { resp := s.makeResponse(req) for _, h := range res.ReverseHosts { hdr := dns.RR_Header{ @@ -77,7 +77,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { } d.Res = resp - } else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteEtcHosts { + } else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteAutoHosts { resp := s.makeResponse(req) name := host diff --git a/internal/dnsforward/msg.go b/internal/dnsforward/msg.go index d4fd4937..3c381704 100644 --- a/internal/dnsforward/msg.go +++ b/internal/dnsforward/msg.go @@ -39,8 +39,11 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu // If the query was filtered by "Safe search", dnsfilter also must return // the IP address that must be used in response. // In this case regardless of the filtering method, we should return it - if result.Reason == dnsfilter.FilteredSafeSearch && result.IP != nil { - return s.genResponseWithIP(m, result.IP) + if result.Reason == dnsfilter.FilteredSafeSearch && + len(result.Rules) > 0 && + result.Rules[0].IP != nil { + + return s.genResponseWithIP(m, result.Rules[0].IP) } if s.conf.BlockingMode == "null_ip" { @@ -68,8 +71,8 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu // Default blocking mode // If there's an IP specified in the rule, return it // For host-type rules, return null IP - if result.IP != nil { - return s.genResponseWithIP(m, result.IP) + if len(result.Rules) > 0 && result.Rules[0].IP != nil { + return s.genResponseWithIP(m, result.Rules[0].IP) } return s.makeResponseNullIP(m) diff --git a/internal/home/controlfiltering.go b/internal/home/controlfiltering.go index e7fb80ba..3fe07e7e 100644 --- a/internal/home/controlfiltering.go +++ b/internal/home/controlfiltering.go @@ -346,10 +346,22 @@ func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request enableFilters(true) } +type checkHostRespRule struct { + FilterListID int64 `json:"filter_list_id"` + Text string `json:"text"` +} + type checkHostResp struct { - Reason string `json:"reason"` - FilterID int64 `json:"filter_id"` - Rule string `json:"rule"` + Reason string `json:"reason"` + + // FilterID is the ID of the rule's filter list. + // + // Deprecated: Use Rules[*].FilterListID. + FilterID int64 `json:"filter_id"` + + Rule string `json:"rule"` + + Rules []*checkHostRespRule `json:"rules"` // for FilteredBlockedService: SvcName string `json:"service_name"` @@ -374,11 +386,20 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) { resp := checkHostResp{} resp.Reason = result.Reason.String() - resp.FilterID = result.FilterID - resp.Rule = result.Rule + resp.FilterID = result.Rules[0].FilterListID + resp.Rule = result.Rules[0].Text resp.SvcName = result.ServiceName resp.CanonName = result.CanonName resp.IPList = result.IPList + + resp.Rules = make([]*checkHostRespRule, len(result.Rules)) + for i, r := range result.Rules { + resp.Rules[i] = &checkHostRespRule{ + FilterListID: r.FilterListID, + Text: r.Text, + } + } + js, err := json.Marshal(resp) if err != nil { httpError(w, http.StatusInternalServerError, "json encode: %s", err) diff --git a/internal/home/home.go b/internal/home/home.go index acf6c3e0..58c99ff6 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -58,7 +58,7 @@ type homeContext struct { dnsServer *dnsforward.Server // DNS module rdns *RDNS // rDNS module whois *Whois // WHOIS module - dnsFilter *dnsfilter.Dnsfilter // DNS filtering module + dnsFilter *dnsfilter.DNSFilter // DNS filtering module dhcpServer *dhcpd.Server // DHCP module auth *Auth // HTTP authentication module filters Filtering // DNS filtering module diff --git a/internal/querylog/decode.go b/internal/querylog/decode.go index 93a1d265..f5937781 100644 --- a/internal/querylog/decode.go +++ b/internal/querylog/decode.go @@ -9,7 +9,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/golibs/log" - "github.com/miekg/dns" ) type logEntryHandler (func(t json.Token, ent *logEntry) error) @@ -85,6 +84,29 @@ var logEntryHandlers = map[string]logEntryHandler{ ent.OrigAnswer, err = base64.StdEncoding.DecodeString(v) return err }, + "Upstream": func(t json.Token, ent *logEntry) error { + v, ok := t.(string) + if !ok { + return nil + } + ent.Upstream = v + return nil + }, + "Elapsed": func(t json.Token, ent *logEntry) error { + v, ok := t.(json.Number) + if !ok { + return nil + } + i, err := v.Int64() + if err != nil { + return err + } + ent.Elapsed = time.Duration(i) + return nil + }, +} + +var resultHandlers = map[string]logEntryHandler{ "IsFiltered": func(t json.Token, ent *logEntry) error { v, ok := t.(bool) if !ok { @@ -94,23 +116,40 @@ var logEntryHandlers = map[string]logEntryHandler{ return nil }, "Rule": func(t json.Token, ent *logEntry) error { - v, ok := t.(string) + s, ok := t.(string) if !ok { return nil } - ent.Result.Rule = v + + l := len(ent.Result.Rules) + if l == 0 { + ent.Result.Rules = []*dnsfilter.ResultRule{{}} + l++ + } + + ent.Result.Rules[l-1].Text = s + return nil }, "FilterID": func(t json.Token, ent *logEntry) error { - v, ok := t.(json.Number) + n, ok := t.(json.Number) if !ok { return nil } - i, err := v.Int64() + + i, err := n.Int64() if err != nil { return err } - ent.Result.FilterID = i + + l := len(ent.Result.Rules) + if l == 0 { + ent.Result.Rules = []*dnsfilter.ResultRule{{}} + l++ + } + + ent.Result.Rules[l-1].FilterListID = i + return nil }, "Reason": func(t json.Token, ent *logEntry) error { @@ -133,62 +172,50 @@ var logEntryHandlers = map[string]logEntryHandler{ ent.Result.ServiceName = v return nil }, - "Upstream": func(t json.Token, ent *logEntry) error { - v, ok := t.(string) - if !ok { - return nil - } - ent.Upstream = v - return nil - }, - "Elapsed": func(t json.Token, ent *logEntry) error { - v, ok := t.(json.Number) - if !ok { - return nil - } - i, err := v.Int64() +} + +func decodeResult(dec *json.Decoder, ent *logEntry) { + for { + keyToken, err := dec.Token() if err != nil { - return err + if err != io.EOF { + log.Debug("decodeResult err: %s", err) + } + + return } - ent.Elapsed = time.Duration(i) - return nil - }, - "Result": func(json.Token, *logEntry) error { - return nil - }, - "Question": func(t json.Token, ent *logEntry) error { - v, ok := t.(string) + + if d, ok := keyToken.(json.Delim); ok { + if d == '}' { + return + } + + continue + } + + key, ok := keyToken.(string) if !ok { - return nil + log.Debug("decodeResult: keyToken is %T (%[1]v) and not string", keyToken) + + return } - var qstr []byte - qstr, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return err - } - q := new(dns.Msg) - err = q.Unpack(qstr) - if err != nil { - return err - } - ent.QHost = q.Question[0].Name - if len(ent.QHost) == 0 { - return nil // nil??? - } - ent.QHost = ent.QHost[:len(ent.QHost)-1] - ent.QType = dns.TypeToString[q.Question[0].Qtype] - ent.QClass = dns.ClassToString[q.Question[0].Qclass] - return nil - }, - "Time": func(t json.Token, ent *logEntry) error { - v, ok := t.(string) + + handler, ok := resultHandlers[key] if !ok { - return nil + continue } - var err error - ent.Time, err = time.Parse(time.RFC3339, v) - return err - }, + + val, err := dec.Token() + if err != nil { + return + } + + if err = handler(val, ent); err != nil { + log.Debug("decodeResult handler err: %s", err) + + return + } + } } func decodeLogEntry(ent *logEntry, str string) { @@ -200,18 +227,27 @@ func decodeLogEntry(ent *logEntry, str string) { if err != io.EOF { log.Debug("decodeLogEntry err: %s", err) } + return } + if _, ok := keyToken.(json.Delim); ok { continue } key, ok := keyToken.(string) if !ok { - log.Debug("decodeLogEntry: keyToken is %T and not string", keyToken) + log.Debug("decodeLogEntry: keyToken is %T (%[1]v) and not string", keyToken) + return } + if key == "Result" { + decodeResult(dec, ent) + + continue + } + handler, ok := logEntryHandlers[key] if !ok { continue @@ -223,7 +259,8 @@ func decodeLogEntry(ent *logEntry, str string) { } if err = handler(val, ent); err != nil { - log.Debug("decodeLogEntry err: %s", err) + log.Debug("decodeLogEntry handler err: %s", err) + return } } diff --git a/internal/querylog/decode_test.go b/internal/querylog/decode_test.go index 02ee77df..d9cbd600 100644 --- a/internal/querylog/decode_test.go +++ b/internal/querylog/decode_test.go @@ -21,29 +21,17 @@ func TestDecode_decodeQueryLog(t *testing.T) { log string want string }{{ - name: "back_compatibility_all_right", - log: `{"Question":"ULgBAAABAAAAAAAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAAB","Answer":"ULiBgAABAAAAAQAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAABwBgABgABAAADQgBLB25zLTE2MjIJYXdzZG5zLTEwAmNvAnVrABFhd3NkbnMtaG9zdG1hc3RlcgZhbWF6b24DY29tAAAAAAEAABwgAAADhAASdQAAAVGA","Result":{},"Time":"2020-11-13T12:41:25.970861+03:00","Elapsed":244066501,"IP":"127.0.0.1","Upstream":"https://1.1.1.1:443/dns-query"}`, - want: "default", - }, { - name: "back_compatibility_bad_msg", - log: `{"Question":"","Answer":"ULiBgAABAAAAAQAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAABwBgABgABAAADQgBLB25zLTE2MjIJYXdzZG5zLTEwAmNvAnVrABFhd3NkbnMtaG9zdG1hc3RlcgZhbWF6b24DY29tAAAAAAEAABwgAAADhAASdQAAAVGA","Result":{},"Time":"2020-11-13T12:41:25.970861+03:00","Elapsed":244066501,"IP":"127.0.0.1","Upstream":"https://1.1.1.1:443/dns-query"}`, - want: "decodeLogEntry err: dns: overflow unpacking uint16\n", - }, { - name: "back_compatibility_bad_decoding", - log: `{"Question":"LgBAAABAAAAAAAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAAB","Answer":"ULiBgAABAAAAAQAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAABwBgABgABAAADQgBLB25zLTE2MjIJYXdzZG5zLTEwAmNvAnVrABFhd3NkbnMtaG9zdG1hc3RlcgZhbWF6b24DY29tAAAAAAEAABwgAAADhAASdQAAAVGA","Result":{},"Time":"2020-11-13T12:41:25.970861+03:00","Elapsed":244066501,"IP":"127.0.0.1","Upstream":"https://1.1.1.1:443/dns-query"}`, - want: "decodeLogEntry err: illegal base64 data at input byte 48\n", - }, { - name: "modern_all_right", + name: "all_right", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, want: "default", }, { name: "bad_filter_id", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1.5},"Elapsed":837429}`, - want: "decodeLogEntry err: strconv.ParseInt: parsing \"1.5\": invalid syntax\n", + want: "decodeResult handler err: strconv.ParseInt: parsing \"1.5\": invalid syntax\n", }, { name: "bad_is_filtered", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":trooe,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, - want: "default", + want: "decodeLogEntry err: invalid character 'o' in literal true (expecting 'u')\n", }, { name: "bad_elapsed", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":-1}`, @@ -55,7 +43,7 @@ func TestDecode_decodeQueryLog(t *testing.T) { }, { name: "bad_time", log: `{"IP":"127.0.0.1","T":"12/09/1998T15:00:00.000000+05:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, - want: "decodeLogEntry err: parsing time \"12/09/1998T15:00:00.000000+05:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"9/1998T15:00:00.000000+05:00\" as \"2006\"\n", + want: "decodeLogEntry handler err: parsing time \"12/09/1998T15:00:00.000000+05:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"9/1998T15:00:00.000000+05:00\" as \"2006\"\n", }, { name: "bad_host", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":6,"QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, @@ -75,7 +63,7 @@ func TestDecode_decodeQueryLog(t *testing.T) { }, { name: "very_bad_client_proto", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"dog","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, - want: "decodeLogEntry err: invalid client proto: \"dog\"\n", + want: "decodeLogEntry handler err: invalid client proto: \"dog\"\n", }, { name: "bad_answer", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":0.9,"Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, @@ -83,7 +71,7 @@ func TestDecode_decodeQueryLog(t *testing.T) { }, { name: "very_bad_answer", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1},"Elapsed":837429}`, - want: "decodeLogEntry err: illegal base64 data at input byte 61\n", + want: "decodeLogEntry handler err: illegal base64 data at input byte 61\n", }, { name: "bad_rule", log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":false,"FilterID":1},"Elapsed":837429}`, @@ -102,7 +90,7 @@ func TestDecode_decodeQueryLog(t *testing.T) { l := &logEntry{} decodeLogEntry(l, tc.log) - assert.True(t, strings.HasSuffix(logOutput.String(), tc.want), logOutput.String()) + assert.True(t, strings.HasSuffix(logOutput.String(), tc.want), "%q\ndoes not end with\n%q", logOutput.String(), tc.want) logOutput.Reset() }) diff --git a/internal/querylog/json.go b/internal/querylog/json.go index 4130ce9e..3beeb0f1 100644 --- a/internal/querylog/json.go +++ b/internal/querylog/json.go @@ -6,10 +6,13 @@ import ( "strconv" "time" + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/golibs/log" "github.com/miekg/dns" ) +// TODO(a.garipov): Use a proper structured approach here. + // Get Client IP address func (l *queryLog) getClientIP(clientIP string) string { if l.conf.AnonymizeClientIP { @@ -29,10 +32,12 @@ func (l *queryLog) getClientIP(clientIP string) string { return clientIP } -// entriesToJSON - converts log entries to JSON -func (l *queryLog) entriesToJSON(entries []*logEntry, oldest time.Time) map[string]interface{} { - // init the response object - data := []map[string]interface{}{} +// jobject is a JSON object alias. +type jobject = map[string]interface{} + +// entriesToJSON converts query log entries to JSON. +func (l *queryLog) entriesToJSON(entries []*logEntry, oldest time.Time) (res jobject) { + data := []jobject{} // the elements order is already reversed (from newer to older) for i := 0; i < len(entries); i++ { @@ -41,17 +46,18 @@ func (l *queryLog) entriesToJSON(entries []*logEntry, oldest time.Time) map[stri data = append(data, jsonEntry) } - result := map[string]interface{}{} - result["oldest"] = "" - if !oldest.IsZero() { - result["oldest"] = oldest.Format(time.RFC3339Nano) + res = jobject{ + "data": data, + "oldest": "", + } + if !oldest.IsZero() { + res["oldest"] = oldest.Format(time.RFC3339Nano) } - result["data"] = data - return result + return res } -func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { +func (l *queryLog) logEntryToJSONEntry(entry *logEntry) (jsonEntry jobject) { var msg *dns.Msg if len(entry.Answer) > 0 { @@ -62,17 +68,18 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { } } - jsonEntry := map[string]interface{}{ + jsonEntry = jobject{ "reason": entry.Result.Reason.String(), "elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64), "time": entry.Time.Format(time.RFC3339Nano), "client": l.getClientIP(entry.IP), "client_proto": entry.ClientProto, - } - jsonEntry["question"] = map[string]interface{}{ - "host": entry.QHost, - "type": entry.QType, - "class": entry.QClass, + "upstream": entry.Upstream, + "question": jobject{ + "host": entry.QHost, + "type": entry.QType, + "class": entry.QClass, + }, } if msg != nil { @@ -83,12 +90,15 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { if opt != nil { dnssecOk = opt.Do() } + jsonEntry["answer_dnssec"] = dnssecOk } - if len(entry.Result.Rule) > 0 { - jsonEntry["rule"] = entry.Result.Rule - jsonEntry["filterId"] = entry.Result.FilterID + jsonEntry["rules"] = resultRulesToJSONRules(entry.Result.Rules) + + if len(entry.Result.Rules) > 0 && len(entry.Result.Rules[0].Text) > 0 { + jsonEntry["rule"] = entry.Result.Rules[0].Text + jsonEntry["filterId"] = entry.Result.Rules[0].FilterListID } if len(entry.Result.ServiceName) != 0 { @@ -113,20 +123,30 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { } } - jsonEntry["upstream"] = entry.Upstream - return jsonEntry } -func answerToMap(a *dns.Msg) []map[string]interface{} { +func resultRulesToJSONRules(rules []*dnsfilter.ResultRule) (jsonRules []jobject) { + jsonRules = make([]jobject, len(rules)) + for i, r := range rules { + jsonRules[i] = jobject{ + "filter_list_id": r.FilterListID, + "text": r.Text, + } + } + + return jsonRules +} + +func answerToMap(a *dns.Msg) (answers []jobject) { if a == nil || len(a.Answer) == 0 { return nil } - answers := []map[string]interface{}{} + answers = []jobject{} for _, k := range a.Answer { header := k.Header() - answer := map[string]interface{}{ + answer := jobject{ "type": dns.TypeToString[header.Rrtype], "ttl": header.Ttl, } diff --git a/internal/querylog/qlog_test.go b/internal/querylog/qlog_test.go index b6651158..fd37a1de 100644 --- a/internal/querylog/qlog_test.go +++ b/internal/querylog/qlog_test.go @@ -236,10 +236,12 @@ func addEntry(l *queryLog, host, answerStr, client string) { a.Answer = append(a.Answer, answer) res := dnsfilter.Result{ IsFiltered: true, - Rule: "SomeRule", Reason: dnsfilter.ReasonRewrite, ServiceName: "SomeService", - FilterID: 1, + Rules: []*dnsfilter.ResultRule{{ + FilterListID: 1, + Text: "SomeRule", + }}, } params := AddParams{ Question: &q, diff --git a/internal/querylog/searchcriteria.go b/internal/querylog/searchcriteria.go index bb573ea6..52b76459 100644 --- a/internal/querylog/searchcriteria.go +++ b/internal/querylog/searchcriteria.go @@ -117,7 +117,7 @@ func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { res.Reason.In( dnsfilter.NotFilteredWhiteList, dnsfilter.ReasonRewrite, - dnsfilter.RewriteEtcHosts, + dnsfilter.RewriteAutoHosts, ) case filteringStatusBlocked: @@ -137,7 +137,7 @@ func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { return res.Reason == dnsfilter.NotFilteredWhiteList case filteringStatusRewritten: - return res.Reason.In(dnsfilter.ReasonRewrite, dnsfilter.RewriteEtcHosts) + return res.Reason.In(dnsfilter.ReasonRewrite, dnsfilter.RewriteAutoHosts) case filteringStatusSafeSearch: return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeSearch diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 73bb637b..0707d47f 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/mod v0.4.0 // indirect golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7 gopkg.in/yaml.v2 v2.4.0 // indirect - honnef.co/go/tools v0.0.1-2020.1.6 + honnef.co/go/tools v0.1.0 mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 ) diff --git a/internal/tools/go.sum b/internal/tools/go.sum index 968ac8a4..a8b20e9e 100644 --- a/internal/tools/go.sum +++ b/internal/tools/go.sum @@ -123,6 +123,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= @@ -158,6 +159,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.1-2020.1.6 h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc= honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY= +honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= +honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 h1:5ZmJGYyuTlhdlIpRxSFhdJqkXQweXETFCEaLhRAX3e8= mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475/go.mod h1:E4LOcu9JQEtnYXtB1Y51drqh2Qr2Ngk9J3YrRCwcbd0= mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY= diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index c4bbed51..f40dd490 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -1,5 +1,37 @@ # AdGuard Home API Change Log + + +## v0.105: API changes + +### Multiple matched rules in `GET /filtering/check_host` and `GET /querylog` + + + +* The properties `rule` and `filter_id` are now deprecated. API users should + inspect the newly-added `rules` object array instead. Currently, it's either + empty or contains one object, which contains the same things as the old two + properties did, but under more correct names: + + ```js + { + // … + + // Deprecated. + "rule": "||example.com^", + // Deprecated. + "filter_id": 42, + // Newly-added. + "rules": [{ + "text": "||example.com^", + "filter_list_id": 42 + }] + } + ``` + + The old fields will be removed in v0.106.0. + ## v0.103: API changes ### API: replace settings in GET /control/dns_info & POST /control/dns_config diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d2329c31..17e9f3f5 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -2,9 +2,9 @@ 'info': 'title': 'AdGuard Home' 'description': > - AdGuard Home REST API. Our admin web interface is built on top of this REST - API. - 'version': '0.104' + AdGuard Home REST-ish API. Our admin web interface is built on top of this + REST-ish API. + 'version': '0.105' 'contact': 'name': 'AdGuard Home' 'url': 'https://github.com/AdguardTeam/AdGuardHome' @@ -1259,11 +1259,26 @@ - 'FilteredBlockedService' - 'ReasonRewrite' 'filter_id': + 'deprecated': true + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. + + Deprecated: use `rules[*].filter_list_id` instead. 'type': 'integer' 'rule': + 'deprecated': true 'type': 'string' 'example': '||example.org^' - 'description': 'Filtering rule applied to the request (if any)' + 'description': > + Filtering rule applied to the request (if any). + + Deprecated: use `rules[*].text` instead. + 'rules': + 'description': 'Applied rules.' + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ResultRule' 'service_name': 'type': 'string' 'description': 'Set if reason=FilteredBlockedService' @@ -1610,15 +1625,27 @@ 'question': '$ref': '#/components/schemas/DnsQuestion' 'filterId': + 'deprecated': true 'type': 'integer' 'example': 123123 'description': > In case if there's a rule applied to this DNS request, this is ID of - the filter that rule belongs to. + the filter list that the rule belongs to. + + Deprecated: use `rules[*].filter_list_id` instead. 'rule': + 'deprecated': true 'type': 'string' 'example': '||example.org^' - 'description': 'Filtering rule applied to the request (if any)' + 'description': > + Filtering rule applied to the request (if any). + + Deprecated: use `rules[*].text` instead. + 'rules': + 'description': 'Applied rules.' + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ResultRule' 'reason': 'type': 'string' 'description': 'DNS filter status' @@ -1668,6 +1695,22 @@ 'anonymize_client_ip': 'type': 'boolean' 'description': "Anonymize clients' IP addresses" + 'ResultRule': + 'description': 'Applied rule.' + 'properties': + 'filter_list_id': + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. + 'example': 123123 + 'format': 'int64' + 'type': 'integer' + 'text': + 'description': > + The text of the filtering rule applied to the request (if any). + 'example': '||example.org^' + 'type': 'string' + 'type': 'object' 'TlsConfig': 'type': 'object' 'description': 'TLS configuration settings and status' diff --git a/scripts/go-lint.sh b/scripts/go-lint.sh index e54cd53e..c133379b 100644 --- a/scripts/go-lint.sh +++ b/scripts/go-lint.sh @@ -112,4 +112,4 @@ exit_on_output sh -c ' { grep -e "defer" -e "_test\.go:" -v || exit 0; } ' -staticcheck --checks='all' ./... +staticcheck ./... diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 00000000..43639bf6 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,14 @@ +checks = ["all"] +initialisms = [ + # See https://github.com/dominikh/go-tools/blob/master/config/config.go. + "inherit" +, "DHCP" +, "DOH" +, "DOQ" +, "DOT" +, "EDNS" +, "QUIC" +, "SDNS" +] +dot_import_whitelist = [] +http_status_code_whitelist = []