* 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)
This commit is contained in:
Simon Zolin
2019-10-09 19:51:26 +03:00
parent b43c076c4d
commit a59e346d4a
14 changed files with 578 additions and 588 deletions

View File

@@ -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
//