From 783ac967a1afb1da1d8c6d610585e983f5846060 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 15 Mar 2019 16:02:48 +0300 Subject: [PATCH 01/12] * filter: refactor --- filter.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/filter.go b/filter.go index 9625b207..34f5093a 100644 --- a/filter.go +++ b/filter.go @@ -139,13 +139,19 @@ func parseFilterContents(contents []byte) (int, string, []string) { // Count lines in the filter for _, line := range lines { + line = strings.TrimSpace(line) - if len(line) > 0 && line[0] == '!' { - if m := filterTitleRegexp.FindAllStringSubmatch(line, -1); len(m) > 0 && len(m[0]) >= 2 && !seenTitle { + if len(line) == 0 { + continue + } + + if line[0] == '!' { + m := filterTitleRegexp.FindAllStringSubmatch(line, -1) + if len(m) > 0 && len(m[0]) >= 2 && !seenTitle { name = m[0][1] seenTitle = true } - } else if len(line) != 0 { + } else { rulesCount++ } } @@ -266,12 +272,12 @@ func (filter *filter) Path() string { // LastTimeUpdated returns the time when the filter was last time updated func (filter *filter) LastTimeUpdated() time.Time { filterFilePath := filter.Path() - if _, err := os.Stat(filterFilePath); os.IsNotExist(err) { + s, err := os.Stat(filterFilePath) + if os.IsNotExist(err) { // if the filter file does not exist, return 0001-01-01 return time.Time{} } - s, err := os.Stat(filterFilePath) if err != nil { // if the filter file does not exist, return 0001-01-01 return time.Time{} From b4732c83c5e28b846f5b34474928e8b022fe48df Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 15 Mar 2019 16:49:10 +0300 Subject: [PATCH 02/12] * filter: use CRC32 to check whether filter data should be updated --- filter.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/filter.go b/filter.go index 34f5093a..e8f68d43 100644 --- a/filter.go +++ b/filter.go @@ -2,10 +2,10 @@ package main import ( "fmt" + "hash/crc32" "io/ioutil" "os" "path/filepath" - "reflect" "regexp" "strconv" "strings" @@ -28,6 +28,7 @@ type filter struct { Name string `json:"name" yaml:"name"` RulesCount int `json:"rulesCount" yaml:"-"` LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"` + checksum uint32 // checksum of the file data dnsfilter.Filter `yaml:",inline"` } @@ -201,22 +202,22 @@ func (filter *filter) update(force bool) (bool, error) { return false, err } - // Extract filter name and count number of rules - rulesCount, filterName, rules := parseFilterContents(body) - - if filterName != "" { - filter.Name = filterName - } - // Check if the filter has been really changed - if reflect.DeepEqual(filter.Rules, rules) { + checksum := crc32.ChecksumIEEE(body) + if filter.checksum == checksum { log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL) return false, nil } + // Extract filter name and count number of rules + rulesCount, filterName, rules := parseFilterContents(body) log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount) + if filterName != "" { + filter.Name = filterName + } filter.RulesCount = rulesCount filter.Rules = rules + filter.checksum = checksum return true, nil } @@ -259,6 +260,7 @@ func (filter *filter) load() error { filter.RulesCount = rulesCount filter.Rules = rules + filter.checksum = crc32.ChecksumIEEE(filterFileContents) filter.LastUpdated = filter.LastTimeUpdated() return nil From 61b1a30aa163647c6fe7919ca7e4968500ab6307 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 15 Mar 2019 19:41:45 +0300 Subject: [PATCH 03/12] * refactor: move code to loadFilters() --- app.go | 20 +------------------- filter.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/app.go b/app.go index 91f4fbdd..232f1526 100644 --- a/app.go +++ b/app.go @@ -12,7 +12,6 @@ import ( "strconv" "sync" "syscall" - "time" "github.com/AdguardTeam/golibs/log" "github.com/gobuffalo/packr" @@ -101,24 +100,7 @@ func run(args options) { config.BindPort = args.bindPort } - // Load filters from the disk - // And if any filter has zero ID, assign a new one - for i := range config.Filters { - filter := &config.Filters[i] // otherwise we're operating on a copy - if filter.ID == 0 { - filter.ID = assignUniqueFilterID() - } - err = filter.load() - if err != nil { - // This is okay for the first start, the filter will be loaded later - log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err) - // clear LastUpdated so it gets fetched right away - } - - if len(filter.Rules) == 0 { - filter.LastUpdated = time.Time{} - } - } + loadFilters() // Save the updated config err = config.write() diff --git a/filter.go b/filter.go index e8f68d43..e20ad45f 100644 --- a/filter.go +++ b/filter.go @@ -44,6 +44,27 @@ func userFilter() filter { } } +// Load filters from the disk +// And if any filter has zero ID, assign a new one +func loadFilters() { + for i := range config.Filters { + filter := &config.Filters[i] // otherwise we're operating on a copy + if filter.ID == 0 { + filter.ID = assignUniqueFilterID() + } + err := filter.load() + if err != nil { + // This is okay for the first start, the filter will be loaded later + log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err) + // clear LastUpdated so it gets fetched right away + } + + if len(filter.Rules) == 0 { + filter.LastUpdated = time.Time{} + } + } +} + func deduplicateFilters() { // Deduplicate filters i := 0 // output index, used for deletion later From a9b329daf6503e6b46787e89783ceb256601b578 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 12:33:03 +0300 Subject: [PATCH 04/12] - control: use locks when operating on config.Filters array --- control.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/control.go b/control.go index 0bd7a5d7..6ec2f009 100644 --- a/control.go +++ b/control.go @@ -559,12 +559,15 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { } // Check for duplicates + config.RLock() for i := range config.Filters { if config.Filters[i].URL == f.URL { + config.RUnlock() httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) return } } + config.RUnlock() // Set necessary properties f.ID = assignUniqueFilterID() @@ -594,7 +597,20 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { // URL is deemed valid, append it to filters, update config, write new filter file and tell dns to reload it // TODO: since we directly feed filters in-memory, revisit if writing configs is always necessary + config.Lock() + + // Check for duplicates + for i := range config.Filters { + if config.Filters[i].URL == f.URL { + config.Unlock() + httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) + return + } + } + config.Filters = append(config.Filters, f) + config.Unlock() + err = writeAllConfigs() if err != nil { httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err) @@ -632,6 +648,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) { } // go through each element and delete if url matches + config.Lock() newFilters := config.Filters[:0] for _, filter := range config.Filters { if filter.URL != url { @@ -647,6 +664,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) { } // Update the configuration after removing filter files config.Filters = newFilters + config.Unlock() httpUpdateConfigReloadDNSReturnOK(w, r) } @@ -670,6 +688,7 @@ func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) { } found := false + config.Lock() for i := range config.Filters { filter := &config.Filters[i] // otherwise we will be operating on a copy if filter.URL == url { @@ -677,6 +696,7 @@ func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) { found = true } } + config.Unlock() if !found { http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) @@ -708,6 +728,7 @@ func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) { } found := false + config.Lock() for i := range config.Filters { filter := &config.Filters[i] // otherwise we will be operating on a copy if filter.URL == url { @@ -715,6 +736,7 @@ func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) { found = true } } + config.Unlock() if !found { http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) From 56271819eaaee84c22ce55d1421bc8cd7b719a69 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 12:38:37 +0300 Subject: [PATCH 05/12] - control: filtering/add_url: don't call httpError() twice on error while reconfiguring --- control.go | 1 + 1 file changed, 1 insertion(+) diff --git a/control.go b/control.go index 6ec2f009..0dbd9c42 100644 --- a/control.go +++ b/control.go @@ -620,6 +620,7 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { err = reconfigureDNSServer() if err != nil { httpError(w, http.StatusInternalServerError, "Couldn't reconfigure the DNS server: %s", err) + return } _, err = fmt.Fprintf(w, "OK %d rules\n", f.RulesCount) From afa54a13393f8ff8f0cffddd81aedd6fa2ba315d Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 12:52:34 +0300 Subject: [PATCH 06/12] * filters: refactor: don't check Enabled flag inside filter.update() & filter.load() --- filter.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/filter.go b/filter.go index e20ad45f..6540e384 100644 --- a/filter.go +++ b/filter.go @@ -52,15 +52,16 @@ func loadFilters() { if filter.ID == 0 { filter.ID = assignUniqueFilterID() } + + if !filter.Enabled { + // No need to load a filter that is not enabled + continue + } + err := filter.load() if err != nil { // This is okay for the first start, the filter will be loaded later log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err) - // clear LastUpdated so it gets fetched right away - } - - if len(filter.Rules) == 0 { - filter.LastUpdated = time.Time{} } } } @@ -114,6 +115,10 @@ func refreshFiltersIfNecessary(force bool) int { for i := range config.Filters { filter := &config.Filters[i] // otherwise we will be operating on a copy + if !filter.Enabled { + continue + } + if filter.ID == 0 { // protect against users modifying the yaml and removing the ID filter.ID = assignUniqueFilterID() } @@ -188,9 +193,6 @@ func (filter *filter) update(force bool) (bool, error) { if filter.ID == 0 { // protect against users deleting the ID filter.ID = assignUniqueFilterID() } - if !filter.Enabled { - return false, nil - } if !force && time.Since(filter.LastUpdated) <= updatePeriod { return false, nil } @@ -258,11 +260,6 @@ func (filter *filter) save() error { // loads filter contents from the file in dataDir func (filter *filter) load() error { - if !filter.Enabled { - // No need to load a filter that is not enabled - return nil - } - filterFilePath := filter.Path() log.Tracef("Loading filter %d contents to: %s", filter.ID, filterFilePath) From ae2c7d00a95d67f21c72eac9c915701313b3646b Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 14:12:04 +0300 Subject: [PATCH 07/12] * control: enable/disable filter: move code to a separate function * don't start updating all filters after 1 filter has been enabled * unload filter data on disable --- control.go | 26 ++------------------------ filter.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/control.go b/control.go index 0dbd9c42..d1d459f5 100644 --- a/control.go +++ b/control.go @@ -688,24 +688,12 @@ func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) { return } - found := false - config.Lock() - for i := range config.Filters { - filter := &config.Filters[i] // otherwise we will be operating on a copy - if filter.URL == url { - filter.Enabled = true - found = true - } - } - config.Unlock() - + found := filterEnable(url, true) if !found { http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) return } - // kick off refresh of rules from new URLs - refreshFiltersIfNecessary(false) httpUpdateConfigReloadDNSReturnOK(w, r) } @@ -728,17 +716,7 @@ func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) { return } - found := false - config.Lock() - for i := range config.Filters { - filter := &config.Filters[i] // otherwise we will be operating on a copy - if filter.URL == url { - filter.Enabled = false - found = true - } - } - config.Unlock() - + found := filterEnable(url, false) if !found { http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) return diff --git a/filter.go b/filter.go index 6540e384..400073da 100644 --- a/filter.go +++ b/filter.go @@ -44,6 +44,34 @@ func userFilter() filter { } } +// Enable or disable a filter +func filterEnable(url string, enable bool) bool { + r := false + config.Lock() + for i := range config.Filters { + filter := &config.Filters[i] // otherwise we will be operating on a copy + if filter.URL == url { + filter.Enabled = enable + if enable { + e := filter.load() + if e != nil { + // This isn't a fatal error, + // because it may occur when someone removes the file from disk. + // In this case the periodic update task will try to download the file. + filter.LastUpdated = time.Time{} + log.Tracef("%s filter load: %v", url, e) + } + } else { + filter.unload() + } + r = true + break + } + } + config.Unlock() + return r +} + // Load filters from the disk // And if any filter has zero ID, assign a new one func loadFilters() { @@ -284,6 +312,12 @@ func (filter *filter) load() error { return nil } +// Clear filter rules +func (filter *filter) unload() { + filter.Rules = []string{} + filter.RulesCount = 0 +} + // Path to the filter contents func (filter *filter) Path() string { return filepath.Join(config.ourWorkingDir, dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt") From eefdf8449a2ea656f71ec0149c14c2a4716536e8 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 14:57:10 +0300 Subject: [PATCH 08/12] * filters: refactor: remove unused if-branches filter.ID == 0: Useless, because filter ID is assigned either on application load or on filter add. len(filter.Rules) == 0: Useless, because rules are added either on application load or on filter add or on filter enable. --- filter.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/filter.go b/filter.go index 400073da..6dedadc1 100644 --- a/filter.go +++ b/filter.go @@ -147,16 +147,6 @@ func refreshFiltersIfNecessary(force bool) int { continue } - if filter.ID == 0 { // protect against users modifying the yaml and removing the ID - filter.ID = assignUniqueFilterID() - } - - if len(filter.Rules) == 0 { - // Try reloading filter from the disk before updating - // This is useful for the case when we simply enable a previously downloaded filter - _ = filter.load() - } - updated, err := filter.update(force) if err != nil { log.Printf("Failed to update filter %s: %s\n", filter.URL, err) @@ -218,9 +208,6 @@ func parseFilterContents(contents []byte) (int, string, []string) { // If "force" is true -- does not check the filter's LastUpdated field // Call "save" to persist the filter contents func (filter *filter) update(force bool) (bool, error) { - if filter.ID == 0 { // protect against users deleting the ID - filter.ID = assignUniqueFilterID() - } if !force && time.Since(filter.LastUpdated) <= updatePeriod { return false, nil } From 0884116de335c0ce9ac1e5428777ff6db923cbd8 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 15:08:23 +0300 Subject: [PATCH 09/12] * app: refactor: don't rewrite config file after filters are updated --- app.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app.go b/app.go index 232f1526..ba8aee3c 100644 --- a/app.go +++ b/app.go @@ -127,11 +127,6 @@ func run(args options) { // Update filters we've just loaded right away, don't wait for periodic update timer go func() { refreshFiltersIfNecessary(false) - // Save the updated config - err := config.write() - if err != nil { - log.Fatal(err) - } }() // Schedule automatic filters updates go periodicallyRefreshFilters() From b54f540f716d875c0fc7d49d0994e479b843be4e Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 14:41:38 +0300 Subject: [PATCH 10/12] * control: refactor: move filter adding code to a separate function --- control.go | 26 ++++++-------------------- filter.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/control.go b/control.go index d1d459f5..ada72697 100644 --- a/control.go +++ b/control.go @@ -559,15 +559,10 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { } // Check for duplicates - config.RLock() - for i := range config.Filters { - if config.Filters[i].URL == f.URL { - config.RUnlock() - httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) - return - } + if filterExists(f.URL) { + httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) + return } - config.RUnlock() // Set necessary properties f.ID = assignUniqueFilterID() @@ -597,20 +592,11 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { // URL is deemed valid, append it to filters, update config, write new filter file and tell dns to reload it // TODO: since we directly feed filters in-memory, revisit if writing configs is always necessary - config.Lock() - - // Check for duplicates - for i := range config.Filters { - if config.Filters[i].URL == f.URL { - config.Unlock() - httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) - return - } + if !filterAdd(f) { + httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) + return } - config.Filters = append(config.Filters, f) - config.Unlock() - err = writeAllConfigs() if err != nil { httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err) diff --git a/filter.go b/filter.go index 6dedadc1..c294b3b5 100644 --- a/filter.go +++ b/filter.go @@ -72,6 +72,38 @@ func filterEnable(url string, enable bool) bool { return r } +// Return TRUE if a filter with this URL exists +func filterExists(url string) bool { + r := false + config.RLock() + for i := range config.Filters { + if config.Filters[i].URL == url { + r = true + break + } + } + config.RUnlock() + return r +} + +// Add a filter +// Return FALSE if a filter with this URL exists +func filterAdd(f filter) bool { + config.Lock() + + // Check for duplicates + for i := range config.Filters { + if config.Filters[i].URL == f.URL { + config.Unlock() + return false + } + } + + config.Filters = append(config.Filters, f) + config.Unlock() + return true +} + // Load filters from the disk // And if any filter has zero ID, assign a new one func loadFilters() { From d664a9de1d27a375cb7ae42dad0a5db88b4d18bc Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 15 Mar 2019 16:09:43 +0300 Subject: [PATCH 11/12] - filter: update 'LastUpdated' field and 'last-modified' file time even when filter's content is up to date * filters: refactor: don't check 'LastUpdated' inside update() --- control.go | 2 +- filter.go | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/control.go b/control.go index ada72697..14b1506a 100644 --- a/control.go +++ b/control.go @@ -569,7 +569,7 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { f.Enabled = true // Download the filter contents - ok, err := f.update(true) + ok, err := f.update() if err != nil { httpError(w, http.StatusBadRequest, "Couldn't fetch filter from url %s: %s", f.URL, err) return diff --git a/filter.go b/filter.go index c294b3b5..45115414 100644 --- a/filter.go +++ b/filter.go @@ -179,7 +179,11 @@ func refreshFiltersIfNecessary(force bool) int { continue } - updated, err := filter.update(force) + if !force && time.Since(filter.LastUpdated) <= updatePeriod { + continue + } + + updated, err := filter.update() if err != nil { log.Printf("Failed to update filter %s: %s\n", filter.URL, err) continue @@ -193,6 +197,11 @@ func refreshFiltersIfNecessary(force bool) int { } updateCount++ + + } else { + mtime := time.Now() + os.Chtimes(filter.Path(), mtime, mtime) + filter.LastUpdated = mtime } } config.Unlock() @@ -236,14 +245,8 @@ func parseFilterContents(contents []byte) (int, string, []string) { return rulesCount, name, lines } -// Checks for filters updates -// If "force" is true -- does not check the filter's LastUpdated field -// Call "save" to persist the filter contents -func (filter *filter) update(force bool) (bool, error) { - if !force && time.Since(filter.LastUpdated) <= updatePeriod { - return false, nil - } - +// Perform upgrade on a filter +func (filter *filter) update() (bool, error) { log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL) resp, err := client.Get(filter.URL) From 0647f3fe8666a38cbdc7a747279513338e49568a Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 18 Mar 2019 17:23:02 +0300 Subject: [PATCH 12/12] * filters: rework update mechanism so that UI doesn't get locked while update is in progress --- filter.go | 69 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/filter.go b/filter.go index 45115414..819e683e 100644 --- a/filter.go +++ b/filter.go @@ -167,44 +167,83 @@ func periodicallyRefreshFilters() { // Checks filters updates if necessary // If force is true, it ignores the filter.LastUpdated field value +// +// Algorithm: +// . Get the list of filters to be updated +// . For each filter run the download and checksum check operation +// . If filter data hasn't changed, set new update time +// . If filter data has changed, parse it, save it on disk, set new update time +// . Apply changes to the current configuration +// . Restart server func refreshFiltersIfNecessary(force bool) int { - config.Lock() + var updateFilters []filter - // fetch URLs - updateCount := 0 + config.RLock() for i := range config.Filters { - filter := &config.Filters[i] // otherwise we will be operating on a copy + f := &config.Filters[i] // otherwise we will be operating on a copy - if !filter.Enabled { + if !f.Enabled { continue } - if !force && time.Since(filter.LastUpdated) <= updatePeriod { + if !force && time.Since(f.LastUpdated) <= updatePeriod { continue } - updated, err := filter.update() + var uf filter + uf.ID = f.ID + uf.URL = f.URL + uf.checksum = f.checksum + updateFilters = append(updateFilters, uf) + } + config.RUnlock() + + updateCount := 0 + for i := range updateFilters { + uf := &updateFilters[i] + updated, err := uf.update() if err != nil { - log.Printf("Failed to update filter %s: %s\n", filter.URL, err) + log.Printf("Failed to update filter %s: %s\n", uf.URL, err) continue } if updated { // Saving it to the filters dir now - err = filter.save() + err = uf.save() if err != nil { - log.Printf("Failed to save the updated filter %d: %s", filter.ID, err) + log.Printf("Failed to save the updated filter %d: %s", uf.ID, err) continue } - updateCount++ - } else { mtime := time.Now() - os.Chtimes(filter.Path(), mtime, mtime) - filter.LastUpdated = mtime + e := os.Chtimes(uf.Path(), mtime, mtime) + if e != nil { + log.Error("os.Chtimes(): %v", e) + } + uf.LastUpdated = mtime } + + config.Lock() + for k := range config.Filters { + f := &config.Filters[k] + if f.ID != uf.ID || f.URL != uf.URL { + continue + } + f.LastUpdated = uf.LastUpdated + if !updated { + continue + } + + log.Info("Updated filter #%d. Rules: %d -> %d", + f.ID, f.RulesCount, uf.RulesCount) + f.Name = uf.Name + f.Rules = uf.Rules + f.RulesCount = uf.RulesCount + f.checksum = uf.checksum + updateCount++ + } + config.Unlock() } - config.Unlock() if updateCount > 0 && isRunning() { err := reconfigureDNSServer()