From a59e346d4af2a1ac91ea087a0c465e9a1e593f86 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Wed, 9 Oct 2019 19:51:26 +0300
Subject: [PATCH] * dnsfilter: major refactoring

* dnsfilter is controlled by package home, not dnsforward
* move HTTP handlers to dnsfilter/
* apply filtering settings without DNS server restart
* use only 1 goroutine for filters update
* apply new filters quickly (after they are ready to be used)
---
 dnsfilter/dnsfilter.go        | 180 +++++++++++++++++++++++++---------
 dnsfilter/dnsfilter_test.go   |  38 +++----
 dnsfilter/rewrites.go         |  93 ++++++++++++++++++
 dnsfilter/security.go         | 179 +++++++++++++++++++++++++++++++++
 dnsforward/dnsforward.go      | 157 +++++------------------------
 dnsforward/dnsforward_test.go |  26 ++---
 home/config.go                |  17 +++-
 home/control.go               | 146 ---------------------------
 home/control_filtering.go     |  52 ++++------
 home/dns.go                   |  47 ++++-----
 home/dns_rewrites.go          | 104 --------------------
 home/filter.go                |  91 +++++++++++------
 home/helpers.go               |  25 -----
 home/home.go                  |  11 ++-
 14 files changed, 578 insertions(+), 588 deletions(-)
 create mode 100644 dnsfilter/rewrites.go
 create mode 100644 dnsfilter/security.go
 delete mode 100644 home/dns_rewrites.go

diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go
index 14c12844..2902c75a 100644
--- a/dnsfilter/dnsfilter.go
+++ b/dnsfilter/dnsfilter.go
@@ -14,6 +14,7 @@ import (
 	"net/http"
 	"os"
 	"strings"
+	"sync"
 	"sync/atomic"
 	"time"
 
@@ -66,7 +67,7 @@ type Config struct {
 	UsePlainHTTP        bool   `yaml:"-"` // use plain HTTP for requests to parental and safe browsing servers
 	SafeSearchEnabled   bool   `yaml:"safesearch_enabled"`
 	SafeBrowsingEnabled bool   `yaml:"safebrowsing_enabled"`
-	ResolverAddress     string // DNS server address
+	ResolverAddress     string `yaml:"-"` // DNS server address
 
 	SafeBrowsingCacheSize uint `yaml:"safebrowsing_cache_size"` // (in bytes)
 	SafeSearchCacheSize   uint `yaml:"safesearch_cache_size"`   // (in bytes)
@@ -75,13 +76,11 @@ type Config struct {
 
 	Rewrites []RewriteEntry `yaml:"rewrites"`
 
-	// Filtering callback function
-	FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
-}
+	// Called when the configuration is changed by HTTP request
+	ConfigModified func() `yaml:"-"`
 
-type privateConfig struct {
-	parentalServer     string // access via methods
-	safeBrowsingServer string // access via methods
+	// Register an HTTP handler
+	HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
 }
 
 // LookupStats store stats collected during safebrowsing or parental checks
@@ -99,17 +98,30 @@ type Stats struct {
 	Safesearch   LookupStats
 }
 
+// Parameters to pass to filters-initializer goroutine
+type filtersInitializerParams struct {
+	filters map[int]string
+}
+
 // Dnsfilter holds added rules and performs hostname matches against the rules
 type Dnsfilter struct {
 	rulesStorage    *urlfilter.RuleStorage
 	filteringEngine *urlfilter.DNSEngine
+	engineLock      sync.RWMutex
 
 	// HTTP lookups for safebrowsing and parental
 	client    http.Client     // handle for http client -- single instance as recommended by docs
 	transport *http.Transport // handle for http transport used by http client
 
-	Config // for direct access by library users, even a = assignment
-	privateConfig
+	parentalServer     string // access via methods
+	safeBrowsingServer string // access via methods
+
+	Config   // for direct access by library users, even a = assignment
+	confLock sync.RWMutex
+
+	// Channel for passing data to filters-initializer goroutine
+	filtersInitializerChan chan filtersInitializerParams
+	filtersInitializerLock sync.Mutex
 }
 
 // Filter represents a filter list
@@ -119,8 +131,6 @@ type Filter struct {
 	FilePath string `yaml:"-"` // Path to a filtering rules file
 }
 
-//go:generate stringer -type=Reason
-
 // Reason holds an enum detailing why it was filtered or not filtered
 type Reason int
 
@@ -153,25 +163,99 @@ const (
 	ReasonRewrite
 )
 
+var reasonNames = []string{
+	"NotFilteredNotFound",
+	"NotFilteredWhiteList",
+	"NotFilteredError",
+
+	"FilteredBlackList",
+	"FilteredSafeBrowsing",
+	"FilteredParental",
+	"FilteredInvalid",
+	"FilteredSafeSearch",
+	"FilteredBlockedService",
+
+	"Rewrite",
+}
+
 func (r Reason) String() string {
-	names := []string{
-		"NotFilteredNotFound",
-		"NotFilteredWhiteList",
-		"NotFilteredError",
-
-		"FilteredBlackList",
-		"FilteredSafeBrowsing",
-		"FilteredParental",
-		"FilteredInvalid",
-		"FilteredSafeSearch",
-		"FilteredBlockedService",
-
-		"Rewrite",
-	}
-	if uint(r) >= uint(len(names)) {
+	if uint(r) >= uint(len(reasonNames)) {
 		return ""
 	}
-	return names[r]
+	return reasonNames[r]
+}
+
+// GetConfig - get configuration
+func (d *Dnsfilter) GetConfig() RequestFilteringSettings {
+	c := RequestFilteringSettings{}
+	// d.confLock.RLock()
+	c.SafeSearchEnabled = d.Config.SafeSearchEnabled
+	c.SafeBrowsingEnabled = d.Config.SafeBrowsingEnabled
+	c.ParentalEnabled = d.Config.ParentalEnabled
+	// d.confLock.RUnlock()
+	return c
+}
+
+// WriteDiskConfig - write configuration
+func (d *Dnsfilter) WriteDiskConfig(c *Config) {
+	*c = d.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(filters map[int]string, async bool) error {
+	if async {
+		params := filtersInitializerParams{
+			filters: filters,
+		}
+
+		d.filtersInitializerLock.Lock() // prevent multiple writers from adding more than 1 task
+		// remove all pending tasks
+		stop := false
+		for !stop {
+			select {
+			case <-d.filtersInitializerChan:
+				//
+			default:
+				stop = true
+			}
+		}
+
+		d.filtersInitializerChan <- params
+		d.filtersInitializerLock.Unlock()
+		return nil
+	}
+
+	err := d.initFiltering(filters)
+	if err != nil {
+		log.Error("Can't initialize filtering subsystem: %s", err)
+		return err
+	}
+
+	return nil
+}
+
+// Starts initializing new filters by signal from channel
+func (d *Dnsfilter) filtersInitializer() {
+	for {
+		params := <-d.filtersInitializerChan
+		err := d.initFiltering(params.filters)
+		if err != nil {
+			log.Error("Can't initialize filtering subsystem: %s", err)
+			continue
+		}
+	}
+}
+
+// Close - close the object
+func (d *Dnsfilter) Close() {
+	if d != nil && d.transport != nil {
+		d.transport.CloseIdleConnections()
+	}
+	if d.rulesStorage != nil {
+		d.rulesStorage.Close()
+	}
 }
 
 type dnsFilterContext struct {
@@ -294,6 +378,9 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
 func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result {
 	var res Result
 
+	d.confLock.RLock()
+	defer d.confLock.RUnlock()
+
 	for _, r := range d.Rewrites {
 		if r.Domain != host {
 			continue
@@ -704,17 +791,28 @@ func (d *Dnsfilter) initFiltering(filters map[int]string) error {
 		listArray = append(listArray, list)
 	}
 
-	var err error
-	d.rulesStorage, err = urlfilter.NewRuleStorage(listArray)
+	rulesStorage, err := urlfilter.NewRuleStorage(listArray)
 	if err != nil {
 		return fmt.Errorf("urlfilter.NewRuleStorage(): %s", err)
 	}
-	d.filteringEngine = urlfilter.NewDNSEngine(d.rulesStorage)
+	filteringEngine := urlfilter.NewDNSEngine(rulesStorage)
+
+	d.engineLock.Lock()
+	if d.rulesStorage != nil {
+		d.rulesStorage.Close()
+	}
+	d.rulesStorage = rulesStorage
+	d.filteringEngine = filteringEngine
+	d.engineLock.Unlock()
+	log.Debug("initialized filtering engine")
+
 	return nil
 }
 
 // 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) (Result, error) {
+	d.engineLock.RLock()
+	defer d.engineLock.RUnlock()
 	if d.filteringEngine == nil {
 		return Result{}, nil
 	}
@@ -926,27 +1024,21 @@ func New(c *Config, filters map[int]string) *Dnsfilter {
 		err := d.initFiltering(filters)
 		if err != nil {
 			log.Error("Can't initialize filtering subsystem: %s", err)
-			d.Destroy()
+			d.Close()
 			return nil
 		}
 	}
 
+	d.filtersInitializerChan = make(chan filtersInitializerParams, 1)
+	go d.filtersInitializer()
+
+	if d.Config.HTTPRegister != nil { // for tests
+		d.registerSecurityHandlers()
+		d.registerRewritesHandlers()
+	}
 	return d
 }
 
-// Destroy is optional if you want to tidy up goroutines without waiting for them to die off
-// right now it closes idle HTTP connections if there are any
-func (d *Dnsfilter) Destroy() {
-	if d != nil && d.transport != nil {
-		d.transport.CloseIdleConnections()
-	}
-
-	if d.rulesStorage != nil {
-		d.rulesStorage.Close()
-		d.rulesStorage = nil
-	}
-}
-
 //
 // config manipulation helpers
 //
diff --git a/dnsfilter/dnsfilter_test.go b/dnsfilter/dnsfilter_test.go
index 4c574b57..37255b78 100644
--- a/dnsfilter/dnsfilter_test.go
+++ b/dnsfilter/dnsfilter_test.go
@@ -108,7 +108,7 @@ func TestEtcHostsMatching(t *testing.T) {
 	filters := make(map[int]string)
 	filters[0] = text
 	d := NewForTest(nil, filters)
-	defer d.Destroy()
+	defer d.Close()
 
 	d.checkMatchIP(t, "google.com", addr, dns.TypeA)
 	d.checkMatchIP(t, "www.google.com", addr, dns.TypeA)
@@ -133,7 +133,7 @@ func TestSafeBrowsing(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(fmt.Sprintf("%s in %s", tc, _Func()), func(t *testing.T) {
 			d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
-			defer d.Destroy()
+			defer d.Close()
 			gctx.stats.Safebrowsing.Requests = 0
 			d.checkMatch(t, "wmconvirus.narod.ru")
 			d.checkMatch(t, "wmconvirus.narod.ru")
@@ -158,7 +158,7 @@ func TestSafeBrowsing(t *testing.T) {
 
 func TestParallelSB(t *testing.T) {
 	d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	t.Run("group", func(t *testing.T) {
 		for i := 0; i < 100; i++ {
 			t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) {
@@ -175,7 +175,7 @@ func TestParallelSB(t *testing.T) {
 // the only way to verify that custom server option is working is to point it at a server that does serve safebrowsing
 func TestSafeBrowsingCustomServerFail(t *testing.T) {
 	d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		// w.Write("Hello, client")
 		fmt.Fprintln(w, "Hello, client")
@@ -192,14 +192,14 @@ func TestSafeBrowsingCustomServerFail(t *testing.T) {
 
 func TestSafeSearch(t *testing.T) {
 	d := NewForTest(nil, nil)
-	defer d.Destroy()
+	defer d.Close()
 	_, ok := d.SafeSearchDomain("www.google.com")
 	if ok {
 		t.Errorf("Expected safesearch to error when disabled")
 	}
 
 	d = NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	val, ok := d.SafeSearchDomain("www.google.com")
 	if !ok {
 		t.Errorf("Expected safesearch to find result for www.google.com")
@@ -211,7 +211,7 @@ func TestSafeSearch(t *testing.T) {
 
 func TestCheckHostSafeSearchYandex(t *testing.T) {
 	d := NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 
 	// Slice of yandex domains
 	yandex := []string{"yAndeX.ru", "YANdex.COM", "yandex.ua", "yandex.by", "yandex.kz", "www.yandex.com"}
@@ -231,7 +231,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
 
 func TestCheckHostSafeSearchGoogle(t *testing.T) {
 	d := NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 
 	// Slice of google domains
 	googleDomains := []string{"www.google.com", "www.google.im", "www.google.co.in", "www.google.iq", "www.google.is", "www.google.it", "www.google.je"}
@@ -251,7 +251,7 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
 
 func TestSafeSearchCacheYandex(t *testing.T) {
 	d := NewForTest(nil, nil)
-	defer d.Destroy()
+	defer d.Close()
 	domain := "yandex.ru"
 
 	var result Result
@@ -267,7 +267,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
 	}
 
 	d = NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 
 	result, err = d.CheckHost(domain, dns.TypeA, &setts)
 	if err != nil {
@@ -293,7 +293,7 @@ func TestSafeSearchCacheYandex(t *testing.T) {
 
 func TestSafeSearchCacheGoogle(t *testing.T) {
 	d := NewForTest(nil, nil)
-	defer d.Destroy()
+	defer d.Close()
 	domain := "www.google.ru"
 	result, err := d.CheckHost(domain, dns.TypeA, &setts)
 	if err != nil {
@@ -304,7 +304,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
 	}
 
 	d = NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 
 	// Let's lookup for safesearch domain
 	safeDomain, ok := d.SafeSearchDomain(domain)
@@ -352,7 +352,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
 
 func TestParentalControl(t *testing.T) {
 	d := NewForTest(&Config{ParentalEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	d.ParentalSensitivity = 3
 	d.checkMatch(t, "pornhub.com")
 	d.checkMatch(t, "pornhub.com")
@@ -435,7 +435,7 @@ func TestMatching(t *testing.T) {
 			filters := make(map[int]string)
 			filters[0] = test.rules
 			d := NewForTest(nil, filters)
-			defer d.Destroy()
+			defer d.Close()
 
 			ret, err := d.CheckHost(test.hostname, dns.TypeA, &setts)
 			if err != nil {
@@ -472,7 +472,7 @@ func TestClientSettings(t *testing.T) {
 	filters := make(map[int]string)
 	filters[0] = "||example.org^\n"
 	d := NewForTest(&Config{ParentalEnabled: true, SafeBrowsingEnabled: false}, filters)
-	defer d.Destroy()
+	defer d.Close()
 	d.ParentalSensitivity = 3
 
 	// no client settings:
@@ -529,7 +529,7 @@ func TestClientSettings(t *testing.T) {
 
 func BenchmarkSafeBrowsing(b *testing.B) {
 	d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	for n := 0; n < b.N; n++ {
 		hostname := "wmconvirus.narod.ru"
 		ret, err := d.CheckHost(hostname, dns.TypeA, &setts)
@@ -544,7 +544,7 @@ func BenchmarkSafeBrowsing(b *testing.B) {
 
 func BenchmarkSafeBrowsingParallel(b *testing.B) {
 	d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
 			hostname := "wmconvirus.narod.ru"
@@ -561,7 +561,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
 
 func BenchmarkSafeSearch(b *testing.B) {
 	d := NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	for n := 0; n < b.N; n++ {
 		val, ok := d.SafeSearchDomain("www.google.com")
 		if !ok {
@@ -575,7 +575,7 @@ func BenchmarkSafeSearch(b *testing.B) {
 
 func BenchmarkSafeSearchParallel(b *testing.B) {
 	d := NewForTest(&Config{SafeSearchEnabled: true}, nil)
-	defer d.Destroy()
+	defer d.Close()
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
 			val, ok := d.SafeSearchDomain("www.google.com")
diff --git a/dnsfilter/rewrites.go b/dnsfilter/rewrites.go
new file mode 100644
index 00000000..6cc18784
--- /dev/null
+++ b/dnsfilter/rewrites.go
@@ -0,0 +1,93 @@
+// DNS Rewrites
+
+package dnsfilter
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/AdguardTeam/golibs/log"
+)
+
+type rewriteEntryJSON struct {
+	Domain string `json:"domain"`
+	Answer string `json:"answer"`
+}
+
+func (d *Dnsfilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
+
+	arr := []*rewriteEntryJSON{}
+
+	d.confLock.Lock()
+	for _, ent := range d.Config.Rewrites {
+		jsent := rewriteEntryJSON{
+			Domain: ent.Domain,
+			Answer: ent.Answer,
+		}
+		arr = append(arr, &jsent)
+	}
+	d.confLock.Unlock()
+
+	w.Header().Set("Content-Type", "application/json")
+	err := json.NewEncoder(w).Encode(arr)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "json.Encode: %s", err)
+		return
+	}
+}
+
+func (d *Dnsfilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
+
+	jsent := rewriteEntryJSON{}
+	err := json.NewDecoder(r.Body).Decode(&jsent)
+	if err != nil {
+		httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
+		return
+	}
+
+	ent := RewriteEntry{
+		Domain: jsent.Domain,
+		Answer: jsent.Answer,
+	}
+	d.confLock.Lock()
+	d.Config.Rewrites = append(d.Config.Rewrites, ent)
+	d.confLock.Unlock()
+	log.Debug("Rewrites: added element: %s -> %s [%d]",
+		ent.Domain, ent.Answer, len(d.Config.Rewrites))
+
+	d.Config.ConfigModified()
+}
+
+func (d *Dnsfilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
+
+	jsent := rewriteEntryJSON{}
+	err := json.NewDecoder(r.Body).Decode(&jsent)
+	if err != nil {
+		httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
+		return
+	}
+
+	entDel := RewriteEntry{
+		Domain: jsent.Domain,
+		Answer: jsent.Answer,
+	}
+	arr := []RewriteEntry{}
+	d.confLock.Lock()
+	for _, ent := range d.Config.Rewrites {
+		if ent == entDel {
+			log.Debug("Rewrites: removed element: %s -> %s", ent.Domain, ent.Answer)
+			continue
+		}
+		arr = append(arr, ent)
+	}
+	d.Config.Rewrites = arr
+	d.confLock.Unlock()
+
+	d.Config.ConfigModified()
+}
+
+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/dnsfilter/security.go b/dnsfilter/security.go
new file mode 100644
index 00000000..c4ce32de
--- /dev/null
+++ b/dnsfilter/security.go
@@ -0,0 +1,179 @@
+// Parental Control, Safe Browsing, Safe Search
+
+package dnsfilter
+
+import (
+	"bufio"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/AdguardTeam/golibs/log"
+)
+
+func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) {
+	text := fmt.Sprintf(format, args...)
+	log.Info("DNSFilter: %s %s: %s", r.Method, r.URL, text)
+	http.Error(w, text, code)
+}
+
+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) {
+	d.Config.SafeBrowsingEnabled = false
+	d.Config.ConfigModified()
+}
+
+func (d *Dnsfilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
+	data := map[string]interface{}{
+		"enabled": d.Config.SafeBrowsingEnabled,
+	}
+	jsonVal, err := json.Marshal(data)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_, err = w.Write(jsonVal)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
+		return
+	}
+}
+
+func parseParametersFromBody(r io.Reader) (map[string]string, error) {
+	parameters := map[string]string{}
+
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if len(line) == 0 {
+			// skip empty lines
+			continue
+		}
+		parts := strings.SplitN(line, "=", 2)
+		if len(parts) != 2 {
+			return parameters, errors.New("Got invalid request body")
+		}
+		parameters[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
+	}
+
+	return parameters, nil
+}
+
+func (d *Dnsfilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
+	parameters, err := parseParametersFromBody(r.Body)
+	if err != nil {
+		httpError(r, w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
+		return
+	}
+
+	sensitivity, ok := parameters["sensitivity"]
+	if !ok {
+		http.Error(w, "Sensitivity parameter was not specified", 400)
+		return
+	}
+
+	switch sensitivity {
+	case "3":
+		break
+	case "EARLY_CHILDHOOD":
+		sensitivity = "3"
+	case "10":
+		break
+	case "YOUNG":
+		sensitivity = "10"
+	case "13":
+		break
+	case "TEEN":
+		sensitivity = "13"
+	case "17":
+		break
+	case "MATURE":
+		sensitivity = "17"
+	default:
+		http.Error(w, "Sensitivity must be set to valid value", 400)
+		return
+	}
+	i, err := strconv.Atoi(sensitivity)
+	if err != nil {
+		http.Error(w, "Sensitivity must be set to valid value", 400)
+		return
+	}
+	d.Config.ParentalSensitivity = i
+	d.Config.ParentalEnabled = true
+	d.Config.ConfigModified()
+}
+
+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) {
+	data := map[string]interface{}{
+		"enabled": d.Config.ParentalEnabled,
+	}
+	if d.Config.ParentalEnabled {
+		data["sensitivity"] = d.Config.ParentalSensitivity
+	}
+	jsonVal, err := json.Marshal(data)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_, err = w.Write(jsonVal)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
+		return
+	}
+}
+
+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) {
+	d.Config.SafeSearchEnabled = false
+	d.Config.ConfigModified()
+}
+
+func (d *Dnsfilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
+	data := map[string]interface{}{
+		"enabled": d.Config.SafeSearchEnabled,
+	}
+	jsonVal, err := json.Marshal(data)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_, err = w.Write(jsonVal)
+	if err != nil {
+		httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
+		return
+	}
+}
+
+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)
+	d.Config.HTTPRegister("POST", "/control/parental/enable", d.handleParentalEnable)
+	d.Config.HTTPRegister("POST", "/control/parental/disable", d.handleParentalDisable)
+	d.Config.HTTPRegister("GET", "/control/parental/status", d.handleParentalStatus)
+	d.Config.HTTPRegister("POST", "/control/safesearch/enable", d.handleSafeSearchEnable)
+	d.Config.HTTPRegister("POST", "/control/safesearch/disable", d.handleSafeSearchDisable)
+	d.Config.HTTPRegister("GET", "/control/safesearch/status", d.handleSafeSearchStatus)
+}
diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go
index d28889b3..415b1cef 100644
--- a/dnsforward/dnsforward.go
+++ b/dnsforward/dnsforward.go
@@ -3,7 +3,6 @@ package dnsforward
 import (
 	"crypto/tls"
 	"errors"
-	"fmt"
 	"net"
 	"net/http"
 	"strings"
@@ -44,12 +43,6 @@ type Server struct {
 	queryLog  querylog.QueryLog    // Query log instance
 	stats     stats.Stats
 
-	// How many times the server was started
-	// While creating a dnsfilter object,
-	//  we use this value to set s.dnsFilter property only with the most recent settings.
-	startCounter         uint32
-	dnsfilterCreatorChan chan dnsfilterCreatorParams
-
 	AllowedClients         map[string]bool // IP addresses of whitelist clients
 	DisallowedClients      map[string]bool // IP addresses of clients that should be blocked
 	AllowedClientsIPNet    []net.IPNet     // CIDRs of whitelist clients
@@ -60,15 +53,11 @@ type Server struct {
 	conf ServerConfig
 }
 
-type dnsfilterCreatorParams struct {
-	conf    dnsfilter.Config
-	filters map[int]string
-}
-
 // NewServer creates a new instance of the dnsforward.Server
 // Note: this function must be called only once
-func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server {
+func NewServer(dnsFilter *dnsfilter.Dnsfilter, stats stats.Stats, queryLog querylog.QueryLog) *Server {
 	s := &Server{}
+	s.dnsFilter = dnsFilter
 	s.stats = stats
 	s.queryLog = queryLog
 	return s
@@ -76,6 +65,7 @@ func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server {
 
 func (s *Server) Close() {
 	s.Lock()
+	s.dnsFilter = nil
 	s.stats = nil
 	s.queryLog = nil
 	s.Unlock()
@@ -84,11 +74,8 @@ func (s *Server) Close() {
 // FilteringConfig represents the DNS filtering configuration of AdGuard Home
 // The zero FilteringConfig is empty and ready for use.
 type FilteringConfig struct {
-	// Create dnsfilter asynchronously.
-	// Requests won't be filtered until dnsfilter is created.
-	// If "restart" command is received while we're creating an old dnsfilter object,
-	//  we delay creation of the new object until the old one is created.
-	AsyncStartup bool `yaml:"-"`
+	// Filtering callback function
+	FilterHandler func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) `yaml:"-"`
 
 	ProtectionEnabled          bool   `yaml:"protection_enabled"`      // whether or not use any of dnsfilter features
 	FilteringEnabled           bool   `yaml:"filtering_enabled"`       // whether or not use filter lists
@@ -116,8 +103,9 @@ type FilteringConfig struct {
 	// Per-client settings can override this configuration.
 	BlockedServices []string `yaml:"blocked_services"`
 
-	CacheSize        uint `yaml:"cache_size"` // DNS cache size (in bytes)
-	dnsfilter.Config `yaml:",inline"`
+	CacheSize uint `yaml:"cache_size"` // DNS cache size (in bytes)
+
+	DnsfilterConf dnsfilter.Config `yaml:",inline"`
 }
 
 // TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
@@ -140,7 +128,6 @@ type ServerConfig struct {
 	TCPListenAddr            *net.TCPAddr                   // TCP listen address
 	Upstreams                []upstream.Upstream            // Configured upstreams
 	DomainsReservedUpstreams map[string][]upstream.Upstream // Map of domains and lists of configured upstreams
-	Filters                  []dnsfilter.Filter             // A list of filters to use
 	OnDNSRequest             func(d *proxy.DNSContext)
 
 	FilteringConfig
@@ -204,13 +191,18 @@ func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []strin
 
 // startInternal starts without locking
 func (s *Server) startInternal(config *ServerConfig) error {
-	if s.dnsFilter != nil || s.dnsProxy != nil {
+	if s.dnsProxy != nil {
 		return errors.New("DNS server is already started")
 	}
 
-	err := s.initDNSFilter(config)
-	if err != nil {
-		return err
+	if config != nil {
+		s.conf = *config
+	}
+	if len(s.conf.ParentalBlockHost) == 0 {
+		s.conf.ParentalBlockHost = parentalBlockHost
+	}
+	if len(s.conf.SafeBrowsingBlockHost) == 0 {
+		s.conf.SafeBrowsingBlockHost = safeBrowsingBlockHost
 	}
 
 	proxyConfig := proxy.Config{
@@ -228,7 +220,7 @@ func (s *Server) startInternal(config *ServerConfig) error {
 		AllServers:               s.conf.AllServers,
 	}
 
-	err = processIPCIDRArray(&s.AllowedClients, &s.AllowedClientsIPNet, s.conf.AllowedClients)
+	err := processIPCIDRArray(&s.AllowedClients, &s.AllowedClientsIPNet, s.conf.AllowedClients)
 	if err != nil {
 		return err
 	}
@@ -269,97 +261,6 @@ func (s *Server) startInternal(config *ServerConfig) error {
 	return s.dnsProxy.Start()
 }
 
-// Initializes the DNS filter
-func (s *Server) initDNSFilter(config *ServerConfig) error {
-	if config != nil {
-		s.conf = *config
-	}
-
-	var filters map[int]string
-	filters = nil
-	if s.conf.FilteringEnabled {
-		filters = make(map[int]string)
-		for _, f := range s.conf.Filters {
-			if f.ID == 0 {
-				filters[int(f.ID)] = string(f.Data)
-			} else {
-				filters[int(f.ID)] = f.FilePath
-			}
-		}
-	}
-
-	if len(s.conf.ParentalBlockHost) == 0 {
-		s.conf.ParentalBlockHost = parentalBlockHost
-	}
-	if len(s.conf.SafeBrowsingBlockHost) == 0 {
-		s.conf.SafeBrowsingBlockHost = safeBrowsingBlockHost
-	}
-
-	if s.conf.AsyncStartup {
-		params := dnsfilterCreatorParams{
-			conf:    s.conf.Config,
-			filters: filters,
-		}
-		s.startCounter++
-		if s.startCounter == 1 {
-			s.dnsfilterCreatorChan = make(chan dnsfilterCreatorParams, 1)
-			go s.dnsfilterCreator()
-		}
-
-		// remove all pending tasks
-		stop := false
-		for !stop {
-			select {
-			case <-s.dnsfilterCreatorChan:
-				//
-			default:
-				stop = true
-			}
-		}
-
-		s.dnsfilterCreatorChan <- params
-	} else {
-		log.Debug("creating dnsfilter...")
-		f := dnsfilter.New(&s.conf.Config, filters)
-		if f == nil {
-			return fmt.Errorf("could not initialize dnsfilter")
-		}
-		log.Debug("created dnsfilter")
-		s.dnsFilter = f
-	}
-	return nil
-}
-
-func (s *Server) dnsfilterCreator() {
-	for {
-		params := <-s.dnsfilterCreatorChan
-
-		s.Lock()
-		counter := s.startCounter
-		s.Unlock()
-
-		log.Debug("creating dnsfilter...")
-		f := dnsfilter.New(&params.conf, params.filters)
-		if f == nil {
-			log.Error("could not initialize dnsfilter")
-			continue
-		}
-
-		set := false
-		s.Lock()
-		if counter == s.startCounter {
-			s.dnsFilter = f
-			set = true
-		}
-		s.Unlock()
-		if set {
-			log.Debug("created and activated dnsfilter")
-		} else {
-			log.Debug("created dnsfilter")
-		}
-	}
-}
-
 // Stop stops the DNS server
 func (s *Server) Stop() error {
 	s.Lock()
@@ -377,11 +278,6 @@ func (s *Server) stopInternal() error {
 		}
 	}
 
-	if s.dnsFilter != nil {
-		s.dnsFilter.Destroy()
-		s.dnsFilter = nil
-	}
-
 	return nil
 }
 
@@ -607,33 +503,24 @@ func (s *Server) updateStats(d *proxy.DNSContext, elapsed time.Duration, res dns
 
 // filterDNSRequest applies the dnsFilter and sets d.Res if the request was filtered
 func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error) {
-	var res dnsfilter.Result
-	req := d.Req
-	host := strings.TrimSuffix(req.Question[0].Name, ".")
-
-	dnsFilter := s.dnsFilter
-
 	if !s.conf.ProtectionEnabled || s.dnsFilter == nil {
 		return &dnsfilter.Result{}, nil
 	}
 
-	var err error
-
 	clientAddr := ""
 	if d.Addr != nil {
 		clientAddr, _, _ = net.SplitHostPort(d.Addr.String())
 	}
 
-	var setts dnsfilter.RequestFilteringSettings
+	setts := s.dnsFilter.GetConfig()
 	setts.FilteringEnabled = true
-	setts.SafeSearchEnabled = s.conf.SafeSearchEnabled
-	setts.SafeBrowsingEnabled = s.conf.SafeBrowsingEnabled
-	setts.ParentalEnabled = s.conf.ParentalEnabled
 	if s.conf.FilterHandler != nil {
 		s.conf.FilterHandler(clientAddr, &setts)
 	}
 
-	res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, &setts)
+	req := d.Req
+	host := strings.TrimSuffix(req.Question[0].Name, ".")
+	res, err := s.dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, &setts)
 	if err != nil {
 		// Return immediately if there's an error
 		return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
diff --git a/dnsforward/dnsforward_test.go b/dnsforward/dnsforward_test.go
index 94e72d67..2568ef7b 100644
--- a/dnsforward/dnsforward_test.go
+++ b/dnsforward/dnsforward_test.go
@@ -148,7 +148,6 @@ func TestServerRace(t *testing.T) {
 
 func TestSafeSearch(t *testing.T) {
 	s := createTestServer(t)
-	s.conf.SafeSearchEnabled = true
 	err := s.Start(nil)
 	if err != nil {
 		t.Fatalf("Failed to start server: %s", err)
@@ -376,23 +375,24 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
 }
 
 func createTestServer(t *testing.T) *Server {
-	s := NewServer(nil, nil)
+	rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1	host.example.org\n"
+	filters := map[int]string{}
+	filters[0] = rules
+	c := dnsfilter.Config{}
+	c.SafeBrowsingEnabled = true
+	c.SafeBrowsingCacheSize = 1000
+	c.SafeSearchEnabled = true
+	c.SafeSearchCacheSize = 1000
+	c.ParentalCacheSize = 1000
+	c.CacheTime = 30
+
+	f := dnsfilter.New(&c, filters)
+	s := NewServer(f, nil, nil)
 	s.conf.UDPListenAddr = &net.UDPAddr{Port: 0}
 	s.conf.TCPListenAddr = &net.TCPAddr{Port: 0}
 
 	s.conf.FilteringConfig.FilteringEnabled = true
 	s.conf.FilteringConfig.ProtectionEnabled = true
-	s.conf.FilteringConfig.SafeBrowsingEnabled = true
-	s.conf.Filters = make([]dnsfilter.Filter, 0)
-
-	s.conf.SafeBrowsingCacheSize = 1000
-	s.conf.SafeSearchCacheSize = 1000
-	s.conf.ParentalCacheSize = 1000
-	s.conf.CacheTime = 30
-
-	rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1	host.example.org\n"
-	filter := dnsfilter.Filter{ID: 0, Data: []byte(rules)}
-	s.conf.Filters = append(s.conf.Filters, filter)
 	return s
 }
 
diff --git a/home/config.go b/home/config.go
index 726eda90..fcd5e3dc 100644
--- a/home/config.go
+++ b/home/config.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/AdguardTeam/AdGuardHome/dhcpd"
+	"github.com/AdguardTeam/AdGuardHome/dnsfilter"
 	"github.com/AdguardTeam/AdGuardHome/dnsforward"
 	"github.com/AdguardTeam/AdGuardHome/querylog"
 	"github.com/AdguardTeam/AdGuardHome/stats"
@@ -71,7 +72,6 @@ type configuration struct {
 	client           *http.Client
 	stats            stats.Stats       // statistics module
 	queryLog         querylog.QueryLog // query log module
-	filteringStarted bool              // TRUE if filtering module is started
 	auth             *Auth             // HTTP authentication module
 
 	// cached version.json to avoid hammering github.io for each page reload
@@ -79,6 +79,7 @@ type configuration struct {
 	versionCheckLastTime time.Time
 
 	dnsctx      dnsContext
+	dnsFilter   *dnsfilter.Dnsfilter
 	dnsServer   *dnsforward.Server
 	dhcpServer  dhcpd.Server
 	httpServer  *http.Server
@@ -217,10 +218,10 @@ func initConfig() {
 	}
 
 	config.DNS.CacheSize = 4 * 1024 * 1024
-	config.DNS.SafeBrowsingCacheSize = 1 * 1024 * 1024
-	config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024
-	config.DNS.ParentalCacheSize = 1 * 1024 * 1024
-	config.DNS.CacheTime = 30
+	config.DNS.DnsfilterConf.SafeBrowsingCacheSize = 1 * 1024 * 1024
+	config.DNS.DnsfilterConf.SafeSearchCacheSize = 1 * 1024 * 1024
+	config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024
+	config.DNS.DnsfilterConf.CacheTime = 30
 	config.Filters = defaultFilters()
 }
 
@@ -367,6 +368,12 @@ func (c *configuration) write() error {
 		config.DNS.QueryLogInterval = dc.Interval
 	}
 
+	if config.dnsFilter != nil {
+		c := dnsfilter.Config{}
+		config.dnsFilter.WriteDiskConfig(&c)
+		config.DNS.DnsfilterConf = c
+	}
+
 	configFile := config.getConfigFilename()
 	log.Debug("Writing YAML file: %s", configFile)
 	yamlText, err := yaml.Marshal(&config)
diff --git a/home/control.go b/home/control.go
index d18c2a85..1f2eb1fa 100644
--- a/home/control.go
+++ b/home/control.go
@@ -377,142 +377,6 @@ func checkDNS(input string, bootstrap []string) error {
 	return nil
 }
 
-// ------------
-// safebrowsing
-// ------------
-
-func handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) {
-	config.DNS.SafeBrowsingEnabled = true
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-}
-
-func handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) {
-	config.DNS.SafeBrowsingEnabled = false
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-}
-
-func handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
-	data := map[string]interface{}{
-		"enabled": config.DNS.SafeBrowsingEnabled,
-	}
-	jsonVal, err := json.Marshal(data)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	_, err = w.Write(jsonVal)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
-		return
-	}
-}
-
-// --------
-// parental
-// --------
-func handleParentalEnable(w http.ResponseWriter, r *http.Request) {
-	parameters, err := parseParametersFromBody(r.Body)
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
-		return
-	}
-
-	sensitivity, ok := parameters["sensitivity"]
-	if !ok {
-		http.Error(w, "Sensitivity parameter was not specified", 400)
-		return
-	}
-
-	switch sensitivity {
-	case "3":
-		break
-	case "EARLY_CHILDHOOD":
-		sensitivity = "3"
-	case "10":
-		break
-	case "YOUNG":
-		sensitivity = "10"
-	case "13":
-		break
-	case "TEEN":
-		sensitivity = "13"
-	case "17":
-		break
-	case "MATURE":
-		sensitivity = "17"
-	default:
-		http.Error(w, "Sensitivity must be set to valid value", 400)
-		return
-	}
-	i, err := strconv.Atoi(sensitivity)
-	if err != nil {
-		http.Error(w, "Sensitivity must be set to valid value", 400)
-		return
-	}
-	config.DNS.ParentalSensitivity = i
-	config.DNS.ParentalEnabled = true
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-}
-
-func handleParentalDisable(w http.ResponseWriter, r *http.Request) {
-	config.DNS.ParentalEnabled = false
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-}
-
-func handleParentalStatus(w http.ResponseWriter, r *http.Request) {
-	data := map[string]interface{}{
-		"enabled": config.DNS.ParentalEnabled,
-	}
-	if config.DNS.ParentalEnabled {
-		data["sensitivity"] = config.DNS.ParentalSensitivity
-	}
-	jsonVal, err := json.Marshal(data)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
-		return
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	_, err = w.Write(jsonVal)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
-		return
-	}
-}
-
-// ------------
-// safebrowsing
-// ------------
-
-func handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
-	config.DNS.SafeSearchEnabled = true
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-}
-
-func handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
-	config.DNS.SafeSearchEnabled = false
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-}
-
-func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
-	data := map[string]interface{}{
-		"enabled": config.DNS.SafeSearchEnabled,
-	}
-	jsonVal, err := json.Marshal(data)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
-		return
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	_, err = w.Write(jsonVal)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
-		return
-	}
-}
-
 // --------------
 // DNS-over-HTTPS
 // --------------
@@ -543,15 +407,6 @@ func registerControlHandlers() {
 	httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
 	http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
 	httpRegister(http.MethodPost, "/control/update", handleUpdate)
-	httpRegister(http.MethodPost, "/control/safebrowsing/enable", handleSafeBrowsingEnable)
-	httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable)
-	httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus)
-	httpRegister(http.MethodPost, "/control/parental/enable", handleParentalEnable)
-	httpRegister(http.MethodPost, "/control/parental/disable", handleParentalDisable)
-	httpRegister(http.MethodGet, "/control/parental/status", handleParentalStatus)
-	httpRegister(http.MethodPost, "/control/safesearch/enable", handleSafeSearchEnable)
-	httpRegister(http.MethodPost, "/control/safesearch/disable", handleSafeSearchDisable)
-	httpRegister(http.MethodGet, "/control/safesearch/status", handleSafeSearchStatus)
 	httpRegister(http.MethodGet, "/control/dhcp/status", handleDHCPStatus)
 	httpRegister(http.MethodGet, "/control/dhcp/interfaces", handleDHCPInterfaces)
 	httpRegister(http.MethodPost, "/control/dhcp/set_config", handleDHCPSetConfig)
@@ -565,7 +420,6 @@ func registerControlHandlers() {
 	RegisterFilteringHandlers()
 	RegisterTLSHandlers()
 	RegisterClientsHandlers()
-	registerRewritesHandlers()
 	RegisterBlockedServicesHandlers()
 	RegisterAuthHandlers()
 
diff --git a/home/control_filtering.go b/home/control_filtering.go
index 03699953..453e0ec4 100644
--- a/home/control_filtering.go
+++ b/home/control_filtering.go
@@ -86,17 +86,8 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = writeAllConfigs()
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
-		return
-	}
-
-	err = reconfigureDNSServer()
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Couldn't reconfigure the DNS server: %s", err)
-		return
-	}
+	onConfigModified()
+	enableFilters(true)
 
 	_, err = fmt.Fprintf(w, "OK %d rules\n", f.RulesCount)
 	if err != nil {
@@ -121,32 +112,28 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// Stop DNS server:
-	//  we close urlfilter object which in turn closes file descriptors to filter files.
-	// Otherwise, Windows won't allow us to remove the file which is being currently used.
-	_ = config.dnsServer.Stop()
-
 	// go through each element and delete if url matches
 	config.Lock()
-	newFilters := config.Filters[:0]
+	newFilters := []filter{}
 	for _, filter := range config.Filters {
 		if filter.URL != req.URL {
 			newFilters = append(newFilters, filter)
 		} else {
-			// Remove the filter file
-			err := os.Remove(filter.Path())
-			if err != nil && !os.IsNotExist(err) {
-				config.Unlock()
-				httpError(w, http.StatusInternalServerError, "Couldn't remove the filter file: %s", err)
-				return
+			err := os.Rename(filter.Path(), filter.Path()+".old")
+			if err != nil {
+				log.Error("os.Rename: %s: %s", filter.Path(), err)
 			}
-			log.Debug("os.Remove(%s)", filter.Path())
 		}
 	}
 	// Update the configuration after removing filter files
 	config.Filters = newFilters
 	config.Unlock()
-	httpUpdateConfigReloadDNSReturnOK(w, r)
+
+	onConfigModified()
+	enableFilters(true)
+
+	// Note: the old files "filter.txt.old" aren't deleted - it's not really necessary,
+	//  but will require the additional code to run after enableFilters() is finished: i.e. complicated
 }
 
 type filterURLJSON struct {
@@ -173,7 +160,8 @@ func handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	httpUpdateConfigReloadDNSReturnOK(w, r)
+	onConfigModified()
+	enableFilters(true)
 }
 
 func handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
@@ -184,12 +172,13 @@ func handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
 	}
 
 	config.UserRules = strings.Split(string(body), "\n")
-	httpUpdateConfigReloadDNSReturnOK(w, r)
+	_ = writeAllConfigs()
+	enableFilters(true)
 }
 
 func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
-	updated := refreshFiltersIfNecessary(true)
-	fmt.Fprintf(w, "OK %d filters updated\n", updated)
+	beginRefreshFilters()
+	fmt.Fprintf(w, "OK 0 filters updated\n")
 }
 
 type filterJSON struct {
@@ -260,9 +249,8 @@ func handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
 
 	config.DNS.FilteringEnabled = req.Enabled
 	config.DNS.FiltersUpdateIntervalHours = req.Interval
-	httpUpdateConfigReloadDNSReturnOK(w, r)
-
-	returnOK(w)
+	onConfigModified()
+	enableFilters(true)
 }
 
 // RegisterFilteringHandlers - register handlers
diff --git a/home/dns.go b/home/dns.go
index 64c9efe4..8e755e06 100644
--- a/home/dns.go
+++ b/home/dns.go
@@ -55,7 +55,18 @@ func initDNSServer() {
 		HTTPRegister:   httpRegister,
 	}
 	config.queryLog = querylog.New(conf)
-	config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog)
+
+	filterConf := config.DNS.DnsfilterConf
+	bindhost := config.DNS.BindHost
+	if config.DNS.BindHost == "0.0.0.0" {
+		bindhost = "127.0.0.1"
+	}
+	filterConf.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
+	filterConf.ConfigModified = onConfigModified
+	filterConf.HTTPRegister = httpRegister
+	config.dnsFilter = dnsfilter.New(&filterConf, nil)
+
+	config.dnsServer = dnsforward.NewServer(config.dnsFilter, config.stats, config.queryLog)
 
 	sessFilename := filepath.Join(baseDir, "sessions.db")
 	config.auth = InitAuth(sessFilename, config.Users)
@@ -159,34 +170,11 @@ func onDNSRequest(d *proxy.DNSContext) {
 }
 
 func generateServerConfig() (dnsforward.ServerConfig, error) {
-	filters := []dnsfilter.Filter{}
-	userFilter := userFilter()
-	filters = append(filters, dnsfilter.Filter{
-		ID:   userFilter.ID,
-		Data: userFilter.Data,
-	})
-	for _, filter := range config.Filters {
-		if !filter.Enabled {
-			continue
-		}
-		filters = append(filters, dnsfilter.Filter{
-			ID:       filter.ID,
-			FilePath: filter.Path(),
-		})
-	}
-
 	newconfig := dnsforward.ServerConfig{
 		UDPListenAddr:   &net.UDPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
 		TCPListenAddr:   &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
 		FilteringConfig: config.DNS.FilteringConfig,
-		Filters:         filters,
 	}
-	newconfig.AsyncStartup = true
-	bindhost := config.DNS.BindHost
-	if config.DNS.BindHost == "0.0.0.0" {
-		bindhost = "127.0.0.1"
-	}
-	newconfig.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
 
 	if config.TLS.Enabled {
 		newconfig.TLSConfig = config.TLS.TLSConfig
@@ -242,20 +230,18 @@ func startDNSServer() error {
 		return fmt.Errorf("unable to start forwarding DNS server: Already running")
 	}
 
+	enableFilters(false)
+
 	newconfig, err := generateServerConfig()
 	if err != nil {
 		return errorx.Decorate(err, "Couldn't start forwarding DNS server")
 	}
+
 	err = config.dnsServer.Start(&newconfig)
 	if err != nil {
 		return errorx.Decorate(err, "Couldn't start forwarding DNS server")
 	}
 
-	if !config.filteringStarted {
-		config.filteringStarted = true
-		startRefreshFilters()
-	}
-
 	return nil
 }
 
@@ -285,6 +271,9 @@ func stopDNSServer() error {
 	// DNS forward module must be closed BEFORE stats or queryLog because it depends on them
 	config.dnsServer.Close()
 
+	config.dnsFilter.Close()
+	config.dnsFilter = nil
+
 	config.stats.Close()
 	config.stats = nil
 
diff --git a/home/dns_rewrites.go b/home/dns_rewrites.go
deleted file mode 100644
index e58c50d7..00000000
--- a/home/dns_rewrites.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package home
-
-import (
-	"encoding/json"
-	"net/http"
-
-	"github.com/AdguardTeam/AdGuardHome/dnsfilter"
-	"github.com/AdguardTeam/golibs/log"
-)
-
-type rewriteEntryJSON struct {
-	Domain string `json:"domain"`
-	Answer string `json:"answer"`
-}
-
-func handleRewriteList(w http.ResponseWriter, r *http.Request) {
-
-	arr := []*rewriteEntryJSON{}
-
-	config.RLock()
-	for _, ent := range config.DNS.Rewrites {
-		jsent := rewriteEntryJSON{
-			Domain: ent.Domain,
-			Answer: ent.Answer,
-		}
-		arr = append(arr, &jsent)
-	}
-	config.RUnlock()
-
-	w.Header().Set("Content-Type", "application/json")
-	err := json.NewEncoder(w).Encode(arr)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "json.Encode: %s", err)
-		return
-	}
-}
-
-func handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
-
-	jsent := rewriteEntryJSON{}
-	err := json.NewDecoder(r.Body).Decode(&jsent)
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
-		return
-	}
-
-	ent := dnsfilter.RewriteEntry{
-		Domain: jsent.Domain,
-		Answer: jsent.Answer,
-	}
-	config.Lock()
-	config.DNS.Rewrites = append(config.DNS.Rewrites, ent)
-	config.Unlock()
-	log.Debug("Rewrites: added element: %s -> %s [%d]",
-		ent.Domain, ent.Answer, len(config.DNS.Rewrites))
-
-	err = writeAllConfigsAndReloadDNS()
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "%s", err)
-		return
-	}
-
-	returnOK(w)
-}
-
-func handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
-
-	jsent := rewriteEntryJSON{}
-	err := json.NewDecoder(r.Body).Decode(&jsent)
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
-		return
-	}
-
-	entDel := dnsfilter.RewriteEntry{
-		Domain: jsent.Domain,
-		Answer: jsent.Answer,
-	}
-	arr := []dnsfilter.RewriteEntry{}
-	config.Lock()
-	for _, ent := range config.DNS.Rewrites {
-		if ent == entDel {
-			log.Debug("Rewrites: removed element: %s -> %s", ent.Domain, ent.Answer)
-			continue
-		}
-		arr = append(arr, ent)
-	}
-	config.DNS.Rewrites = arr
-	config.Unlock()
-
-	err = writeAllConfigsAndReloadDNS()
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "%s", err)
-		return
-	}
-
-	returnOK(w)
-}
-
-func registerRewritesHandlers() {
-	httpRegister(http.MethodGet, "/control/rewrite/list", handleRewriteList)
-	httpRegister(http.MethodPost, "/control/rewrite/add", handleRewriteAdd)
-	httpRegister(http.MethodPost, "/control/rewrite/delete", handleRewriteDelete)
-}
diff --git a/home/filter.go b/home/filter.go
index be60e2a3..38425301 100644
--- a/home/filter.go
+++ b/home/filter.go
@@ -19,18 +19,13 @@ import (
 var (
 	nextFilterID      = time.Now().Unix() // semi-stable way to generate an unique ID
 	filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
+	forceRefresh      bool
 )
 
 func initFiltering() {
 	loadFilters()
 	deduplicateFilters()
 	updateUniqueFilterID(config.Filters)
-}
-
-func startRefreshFilters() {
-	go func() {
-		_ = refreshFiltersIfNecessary(false)
-	}()
 	go periodicallyRefreshFilters()
 }
 
@@ -180,32 +175,43 @@ func assignUniqueFilterID() int64 {
 
 // Sets up a timer that will be checking for filters updates periodically
 func periodicallyRefreshFilters() {
+	nextRefresh := int64(0)
 	for {
-		time.Sleep(1 * time.Hour)
-		if config.DNS.FiltersUpdateIntervalHours == 0 {
-			continue
+		if forceRefresh {
+			_ = refreshFiltersIfNecessary(true)
+			forceRefresh = false
 		}
 
-		refreshFiltersIfNecessary(false)
+		if config.DNS.FiltersUpdateIntervalHours != 0 && nextRefresh <= time.Now().Unix() {
+			_ = refreshFiltersIfNecessary(false)
+			nextRefresh = time.Now().Add(1 * time.Hour).Unix()
+		}
+		time.Sleep(1 * time.Second)
 	}
 }
 
+// Schedule the procedure to refresh filters
+func beginRefreshFilters() {
+	forceRefresh = true
+	log.Debug("Filters: schedule update")
+}
+
 // 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
-// . Stop server
 // . For each filter:
 //  . If filter data hasn't changed, just set new update time on file
-//  . If filter data has changed, save it on disk
-//  . Apply changes to the current configuration
-// . Start server
+//  . If filter data has changed: rename the old file, store the new data on disk
+//  . Pass new filters to dnsfilter object
 func refreshFiltersIfNecessary(force bool) int {
 	var updateFilters []filter
 	var updateFlags []bool // 'true' if filter data has changed
 
+	log.Debug("Filters: updating...")
+
 	now := time.Now()
 	config.RLock()
 	for i := range config.Filters {
@@ -229,7 +235,6 @@ func refreshFiltersIfNecessary(force bool) int {
 	}
 	config.RUnlock()
 
-	updateCount := 0
 	for i := range updateFilters {
 		uf := &updateFilters[i]
 		updated, err := uf.update()
@@ -239,24 +244,14 @@ func refreshFiltersIfNecessary(force bool) int {
 			continue
 		}
 		uf.LastUpdated = now
-		if updated {
-			updateCount++
-		}
 	}
 
-	stopped := false
-	if updateCount != 0 {
-		_ = config.dnsServer.Stop()
-		stopped = true
-	}
-
-	updateCount = 0
+	updateCount := 0
 	for i := range updateFilters {
 		uf := &updateFilters[i]
 		updated := updateFlags[i]
 		if updated {
-			// Saving it to the filters dir now
-			err := uf.save()
+			err := uf.saveAndBackupOld()
 			if err != nil {
 				log.Printf("Failed to save the updated filter %d: %s", uf.ID, err)
 				continue
@@ -290,12 +285,20 @@ func refreshFiltersIfNecessary(force bool) int {
 		config.Unlock()
 	}
 
-	if stopped {
-		err := reconfigureDNSServer()
-		if err != nil {
-			log.Error("cannot reconfigure DNS server with the new filters: %s", err)
+	if updateCount != 0 {
+		enableFilters(false)
+
+		for i := range updateFilters {
+			uf := &updateFilters[i]
+			updated := updateFlags[i]
+			if !updated {
+				continue
+			}
+			_ = os.Remove(uf.Path() + ".old")
 		}
 	}
+
+	log.Debug("Filters: update finished")
 	return updateCount
 }
 
@@ -413,6 +416,12 @@ func (filter *filter) save() error {
 	return err
 }
 
+func (filter *filter) saveAndBackupOld() error {
+	filterFilePath := filter.Path()
+	_ = os.Rename(filterFilePath, filterFilePath+".old")
+	return filter.save()
+}
+
 // loads filter contents from the file in dataDir
 func (filter *filter) load() error {
 	filterFilePath := filter.Path()
@@ -467,3 +476,23 @@ func (filter *filter) LastTimeUpdated() time.Time {
 	// filter file modified time
 	return s.ModTime()
 }
+
+func enableFilters(async bool) {
+	var filters map[int]string
+	if config.DNS.FilteringConfig.FilteringEnabled {
+		// convert array of filters
+		filters = make(map[int]string)
+
+		userFilter := userFilter()
+		filters[int(userFilter.ID)] = string(userFilter.Data)
+
+		for _, filter := range config.Filters {
+			if !filter.Enabled {
+				continue
+			}
+			filters[int(filter.ID)] = filter.Path()
+		}
+	}
+
+	_ = config.dnsFilter.SetFilters(filters, async)
+}
diff --git a/home/helpers.go b/home/helpers.go
index 756d6b97..6b0f01ed 100644
--- a/home/helpers.go
+++ b/home/helpers.go
@@ -1,12 +1,10 @@
 package home
 
 import (
-	"bufio"
 	"bytes"
 	"context"
 	"errors"
 	"fmt"
-	"io"
 	"net"
 	"net/http"
 	"net/url"
@@ -155,29 +153,6 @@ func postInstallHandler(handler http.Handler) http.Handler {
 	return &postInstallHandlerStruct{handler}
 }
 
-// -------------------------------------------------
-// helper functions for parsing parameters from body
-// -------------------------------------------------
-func parseParametersFromBody(r io.Reader) (map[string]string, error) {
-	parameters := map[string]string{}
-
-	scanner := bufio.NewScanner(r)
-	for scanner.Scan() {
-		line := scanner.Text()
-		if len(line) == 0 {
-			// skip empty lines
-			continue
-		}
-		parts := strings.SplitN(line, "=", 2)
-		if len(parts) != 2 {
-			return parameters, errors.New("Got invalid request body")
-		}
-		parameters[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
-	}
-
-	return parameters, nil
-}
-
 // ------------------
 // network interfaces
 // ------------------
diff --git a/home/home.go b/home/home.go
index 80f9e860..afbb3002 100644
--- a/home/home.go
+++ b/home/home.go
@@ -143,11 +143,12 @@ func run(args options) {
 		}
 
 		initDNSServer()
-
-		err = startDNSServer()
-		if err != nil {
-			log.Fatal(err)
-		}
+		go func() {
+			err = startDNSServer()
+			if err != nil {
+				log.Fatal(err)
+			}
+		}()
 
 		err = startDHCPServer()
 		if err != nil {