filtering: refactor; change API; add "filters_update_interval" setting

+ config: "filters_update_interval"
* add /control/filtering_info
* remove /control/filtering/enable
* remove /control/filtering/disable

* add /control/filtering_config
* remove /control/filtering/status

* add /control/filtering/set_url
* remove /control/filtering/enable_url
* remove /control/filtering/disable_url
This commit is contained in:
Simon Zolin 2019-09-04 14:12:00 +03:00
parent 8c89973365
commit adb422fedf
14 changed files with 324 additions and 157 deletions

View File

@ -45,6 +45,11 @@ Contents:
* Query logs * Query logs
* API: Set querylog parameters * API: Set querylog parameters
* API: Get querylog parameters * API: Get querylog parameters
* Filtering
* Filters update mechanism
* API: Get filtering parameters
* API: Set filtering parameters
* API: Set URL parameters
## Relations between subsystems ## Relations between subsystems
@ -1019,3 +1024,76 @@ Response:
"enabled": true | false "enabled": true | false
"interval": 1 | 7 | 30 | 90 "interval": 1 | 7 | 30 | 90
} }
## Filtering
### Filters update mechanism
Filters can be updated either manually by request from UI or automatically.
Auto-update interval can be configured in UI. If it is 0, auto-update is disabled.
When the last modification date of filter files is older than auto-update interval, auto-update procedure is started.
If an enabled filter file doesn't exist, it's downloaded on application startup. This includes the case when installation wizard is completed and there are no filter files yet.
When auto-update time comes, server starts the update procedure by downloading filter files. After new filter files are in place, it restarts DNS filtering module with new rules.
Only filters that are enabled by configuration can be updated.
As a result of the update procedure, all enabled filter files are written to disk, refreshed (their last modification date is equal to the current time) and loaded.
### API: Get filtering parameters
Request:
GET /control/filtering_info
Response:
200 OK
{
"enabled": true | false
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
"filters":[
{
"id":1
"enabled":true,
"url":"https://...",
"name":"...",
"rules_count":1234,
"last_updated":"2019-09-04T18:29:30+00:00",
}
...
],
"user_rules":["...", ...]
}
### API: Set filtering parameters
Request:
POST /control/filtering_config
{
"enabled": true | false
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
}
Response:
200 OK
### API: Set URL parameters
Request:
POST /control/filtering/set_url
{
"url": "..."
"enabled": true | false
}
Response:
200 OK

View File

@ -114,9 +114,9 @@ type Dnsfilter struct {
// Filter represents a filter list // Filter represents a filter list
type Filter struct { type Filter struct {
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase ID int64 // auto-assigned when filter is added (see nextFilterID)
Data []byte `json:"-" yaml:"-"` // List of rules divided by '\n' Data []byte `yaml:"-"` // List of rules divided by '\n'
FilePath string `json:"-" yaml:"-"` // Path to a filtering rules file FilePath string `yaml:"-"` // Path to a filtering rules file
} }
//go:generate stringer -type=Reason //go:generate stringer -type=Reason

View File

@ -66,8 +66,10 @@ func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server {
// FilteringConfig represents the DNS filtering configuration of AdGuard Home // FilteringConfig represents the DNS filtering configuration of AdGuard Home
// The zero FilteringConfig is empty and ready for use. // The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct { type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600) BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled

1
go.mod
View File

@ -7,7 +7,6 @@ require (
github.com/AdguardTeam/golibs v0.2.1 github.com/AdguardTeam/golibs v0.2.1
github.com/AdguardTeam/urlfilter v0.5.0 github.com/AdguardTeam/urlfilter v0.5.0
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
github.com/etcd-io/bbolt v1.3.3 github.com/etcd-io/bbolt v1.3.3
github.com/go-test/deep v1.0.1 github.com/go-test/deep v1.0.1

View File

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/AdGuardHome/querylog" "github.com/AdguardTeam/AdGuardHome/querylog"
"github.com/AdguardTeam/AdGuardHome/stats" "github.com/AdguardTeam/AdGuardHome/stats"
@ -72,6 +71,7 @@ type configuration struct {
client *http.Client client *http.Client
stats stats.Stats stats stats.Stats
queryLog querylog.QueryLog queryLog querylog.QueryLog
filteringStarted bool
// cached version.json to avoid hammering github.io for each page reload // cached version.json to avoid hammering github.io for each page reload
versionCheckJSON []byte versionCheckJSON []byte
@ -172,16 +172,17 @@ var config = configuration{
Port: 53, Port: 53,
StatsInterval: 1, StatsInterval: 1,
FilteringConfig: dnsforward.FilteringConfig{ FilteringConfig: dnsforward.FilteringConfig{
ProtectionEnabled: true, // whether or not use any of dnsfilter features ProtectionEnabled: true, // whether or not use any of dnsfilter features
FilteringEnabled: true, // whether or not use filter lists FilteringEnabled: true, // whether or not use filter lists
BlockingMode: "nxdomain", // mode how to answer filtered requests FiltersUpdateIntervalHours: 24,
BlockedResponseTTL: 10, // in seconds BlockingMode: "nxdomain", // mode how to answer filtered requests
QueryLogEnabled: true, BlockedResponseTTL: 10, // in seconds
QueryLogInterval: 1, QueryLogEnabled: true,
Ratelimit: 20, QueryLogInterval: 1,
RefuseAny: true, Ratelimit: 20,
BootstrapDNS: defaultBootstrap, RefuseAny: true,
AllServers: false, BootstrapDNS: defaultBootstrap,
AllServers: false,
}, },
UpstreamDNS: defaultDNS, UpstreamDNS: defaultDNS,
}, },
@ -191,12 +192,6 @@ var config = configuration{
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
}, },
}, },
Filters: []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
DHCP: dhcpd.ServerConfig{ DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400, LeaseDuration: 86400,
ICMPTimeout: 1000, ICMPTimeout: 1000,
@ -226,6 +221,7 @@ func initConfig() {
config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024 config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024
config.DNS.ParentalCacheSize = 1 * 1024 * 1024 config.DNS.ParentalCacheSize = 1 * 1024 * 1024
config.DNS.CacheTime = 30 config.DNS.CacheTime = 30
config.Filters = defaultFilters()
} }
// getConfigFilename returns path to the current config file // getConfigFilename returns path to the current config file
@ -276,6 +272,9 @@ func parseConfig() error {
if !checkStatsInterval(config.DNS.StatsInterval) { if !checkStatsInterval(config.DNS.StatsInterval) {
config.DNS.StatsInterval = 1 config.DNS.StatsInterval = 1
} }
if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
config.DNS.FiltersUpdateIntervalHours = 24
}
if !checkQueryLogInterval(config.DNS.QueryLogInterval) { if !checkQueryLogInterval(config.DNS.QueryLogInterval) {
config.DNS.QueryLogInterval = 1 config.DNS.QueryLogInterval = 1
@ -308,11 +307,6 @@ func parseConfig() error {
return err return err
} }
// Deduplicate filters
deduplicateFilters()
updateUniqueFilterID(config.Filters)
return nil return nil
} }

View File

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
@ -17,8 +16,6 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
const updatePeriod = time.Hour * 24
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"} var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
// ---------------- // ----------------
@ -547,15 +544,6 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage) httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
httpRegister(http.MethodPost, "/control/update", handleUpdate) httpRegister(http.MethodPost, "/control/update", handleUpdate)
httpRegister(http.MethodPost, "/control/filtering/enable", handleFilteringEnable)
httpRegister(http.MethodPost, "/control/filtering/disable", handleFilteringDisable)
httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL)
httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL)
httpRegister(http.MethodPost, "/control/filtering/enable_url", handleFilteringEnableURL)
httpRegister(http.MethodPost, "/control/filtering/disable_url", handleFilteringDisableURL)
httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh)
httpRegister(http.MethodGet, "/control/filtering/status", handleFilteringStatus)
httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules)
httpRegister(http.MethodPost, "/control/safebrowsing/enable", handleSafeBrowsingEnable) httpRegister(http.MethodPost, "/control/safebrowsing/enable", handleSafeBrowsingEnable)
httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable) httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable)
httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus) httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus)
@ -575,6 +563,7 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/access/list", handleAccessList) httpRegister(http.MethodGet, "/control/access/list", handleAccessList)
httpRegister(http.MethodPost, "/control/access/set", handleAccessSet) httpRegister(http.MethodPost, "/control/access/set", handleAccessSet)
RegisterFilteringHandlers()
RegisterTLSHandlers() RegisterTLSHandlers()
RegisterClientsHandlers() RegisterClientsHandlers()
registerRewritesHandlers() registerRewritesHandlers()

View File

@ -5,74 +5,57 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"time"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/asaskevich/govalidator"
) )
func handleFilteringEnable(w http.ResponseWriter, r *http.Request) { // IsValidURL - return TRUE if URL is valid
config.DNS.FilteringEnabled = true func IsValidURL(rawurl string) bool {
httpUpdateConfigReloadDNSReturnOK(w, r) url, err := url.ParseRequestURI(rawurl)
if err != nil {
return false //Couldn't even parse the rawurl
}
if len(url.Scheme) == 0 {
return false //No Scheme found
}
return true
} }
func handleFilteringDisable(w http.ResponseWriter, r *http.Request) { type filterAddJSON struct {
config.DNS.FilteringEnabled = false Name string `json:"name"`
httpUpdateConfigReloadDNSReturnOK(w, r) URL string `json:"url"`
}
func handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"enabled": config.DNS.FilteringEnabled,
}
config.RLock()
data["filters"] = config.Filters
data["user_rules"] = config.UserRules
jsonVal, err := json.Marshal(data)
config.RUnlock()
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
}
} }
func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
f := filter{} fj := filterAddJSON{}
err := json.NewDecoder(r.Body).Decode(&f) err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err) httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
return return
} }
if len(f.URL) == 0 { if !IsValidURL(fj.URL) {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest) http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(f.URL); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return return
} }
// Check for duplicates // Check for duplicates
if filterExists(f.URL) { if filterExists(fj.URL) {
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
return return
} }
// Set necessary properties // Set necessary properties
f := filter{
Enabled: true,
URL: fj.URL,
Name: fj.Name,
}
f.ID = assignUniqueFilterID() f.ID = assignUniqueFilterID()
f.Enabled = true
// Download the filter contents // Download the filter contents
ok, err := f.update() ok, err := f.update()
@ -133,7 +116,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
return return
} }
if valid := govalidator.IsRequestURL(req.URL); !valid { if !IsValidURL(req.URL) {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest) http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return return
} }
@ -166,54 +149,27 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
httpUpdateConfigReloadDNSReturnOK(w, r) httpUpdateConfigReloadDNSReturnOK(w, r)
} }
func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) { type filterURLJSON struct {
parameters, err := parseParametersFromBody(r.Body) URL string `json:"url"`
if err != nil { Enabled bool `json:"enabled"`
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
return
}
url, ok := parameters["url"]
if !ok {
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
return
}
if valid := govalidator.IsRequestURL(url); !valid {
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
found := filterEnable(url, true)
if !found {
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest)
return
}
httpUpdateConfigReloadDNSReturnOK(w, r)
} }
func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) { func handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
parameters, err := parseParametersFromBody(r.Body) fj := filterURLJSON{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err) httpError(w, http.StatusBadRequest, "json decode: %s", err)
return return
} }
url, ok := parameters["url"] if !IsValidURL(fj.URL) {
if !ok { http.Error(w, "invalid URL", http.StatusBadRequest)
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
return return
} }
if valid := govalidator.IsRequestURL(url); !valid { found := filterEnable(fj.URL, fj.Enabled)
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
return
}
found := filterEnable(url, false)
if !found { if !found {
http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) http.Error(w, "URL doesn't exist", http.StatusBadRequest)
return return
} }
@ -235,3 +191,91 @@ func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
updated := refreshFiltersIfNecessary(true) updated := refreshFiltersIfNecessary(true)
fmt.Fprintf(w, "OK %d filters updated\n", updated) fmt.Fprintf(w, "OK %d filters updated\n", updated)
} }
type filterJSON struct {
ID int64 `json:"id"`
Enabled bool `json:"enabled"`
URL string `json:"url"`
Name string `json:"name"`
RulesCount uint32 `json:"rules_count"`
LastUpdated string `json:"last_updated"`
}
type filteringConfig struct {
Enabled bool `json:"enabled"`
Interval uint32 `json:"interval"` // in hours
Filters []filterJSON `json:"filters"`
UserRules []string `json:"user_rules"`
}
// Get filtering configuration
func handleFilteringInfo(w http.ResponseWriter, r *http.Request) {
resp := filteringConfig{}
config.RLock()
resp.Enabled = config.DNS.FilteringEnabled
resp.Interval = config.DNS.FiltersUpdateIntervalHours
for _, f := range config.Filters {
fj := filterJSON{
ID: f.ID,
Enabled: f.Enabled,
URL: f.URL,
Name: f.Name,
RulesCount: uint32(f.RulesCount),
}
if f.LastUpdated.Second() != 0 {
fj.LastUpdated = f.LastUpdated.Format(time.RFC3339)
}
resp.Filters = append(resp.Filters, fj)
}
resp.UserRules = config.UserRules
config.RUnlock()
jsonVal, err := json.Marshal(resp)
if err != nil {
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
httpError(w, http.StatusInternalServerError, "http write: %s", err)
}
}
// Set filtering configuration
func handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
req := filteringConfig{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(w, http.StatusBadRequest, "json decode: %s", err)
return
}
if !checkFiltersUpdateIntervalHours(req.Interval) {
httpError(w, http.StatusBadRequest, "Unsupported interval")
return
}
config.DNS.FilteringEnabled = req.Enabled
config.DNS.FiltersUpdateIntervalHours = req.Interval
httpUpdateConfigReloadDNSReturnOK(w, r)
returnOK(w)
}
// RegisterFilteringHandlers - register handlers
func RegisterFilteringHandlers() {
httpRegister(http.MethodGet, "/control/filtering_info", handleFilteringInfo)
httpRegister(http.MethodPost, "/control/filtering_config", handleFilteringConfig)
httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL)
httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL)
httpRegister(http.MethodPost, "/control/filtering/set_url", handleFilteringSetURL)
httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh)
httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules)
}
func checkFiltersUpdateIntervalHours(i uint32) bool {
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
}

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"os/exec" "os/exec"
"path/filepath"
"strconv" "strconv"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@ -239,6 +240,9 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
config.AuthName = newSettings.Username config.AuthName = newSettings.Username
config.AuthPass = newSettings.Password config.AuthPass = newSettings.Password
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
initDNSServer(dnsBaseDir)
err = startDNSServer() err = startDNSServer()
if err != nil { if err != nil {
config.firstRun = true config.firstRun = true
@ -255,8 +259,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return return
} }
go refreshFiltersIfNecessary(false)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTP { if restartHTTP {

View File

@ -49,6 +49,7 @@ func initDNSServer(baseDir string) {
config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog) config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog)
initRDNS() initRDNS()
initFiltering()
} }
func isRunning() bool { func isRunning() bool {
@ -165,6 +166,11 @@ func startDNSServer() error {
return errorx.Decorate(err, "Couldn't start forwarding DNS server") return errorx.Decorate(err, "Couldn't start forwarding DNS server")
} }
if !config.filteringStarted {
config.filteringStarted = true
startRefreshFilters()
}
return nil return nil
} }

View File

@ -21,13 +21,35 @@ var (
filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`) filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
) )
func initFiltering() {
loadFilters()
deduplicateFilters()
updateUniqueFilterID(config.Filters)
}
func startRefreshFilters() {
go func() {
_ = refreshFiltersIfNecessary(false)
}()
go periodicallyRefreshFilters()
}
func defaultFilters() []filter {
return []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
}
}
// field ordering is important -- yaml fields will mirror ordering from here // field ordering is important -- yaml fields will mirror ordering from here
type filter struct { type filter struct {
Enabled bool `json:"enabled"` Enabled bool
URL string `json:"url"` URL string
Name string `json:"name" yaml:"name"` Name string `yaml:"name"`
RulesCount int `json:"rulesCount" yaml:"-"` RulesCount int `yaml:"-"`
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"` LastUpdated time.Time `yaml:"-"`
checksum uint32 // checksum of the file data checksum uint32 // checksum of the file data
dnsfilter.Filter `yaml:",inline"` dnsfilter.Filter `yaml:",inline"`
@ -119,8 +141,7 @@ func loadFilters() {
err := filter.load() err := filter.load()
if err != nil { if err != nil {
// This is okay for the first start, the filter will be loaded later log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err)
} }
} }
} }
@ -159,7 +180,12 @@ func assignUniqueFilterID() int64 {
// Sets up a timer that will be checking for filters updates periodically // Sets up a timer that will be checking for filters updates periodically
func periodicallyRefreshFilters() { func periodicallyRefreshFilters() {
for range time.Tick(time.Minute) { for {
time.Sleep(1 * time.Hour)
if config.DNS.FiltersUpdateIntervalHours == 0 {
continue
}
refreshFiltersIfNecessary(false) refreshFiltersIfNecessary(false)
} }
} }
@ -180,10 +206,7 @@ func refreshFiltersIfNecessary(force bool) int {
var updateFilters []filter var updateFilters []filter
var updateFlags []bool // 'true' if filter data has changed var updateFlags []bool // 'true' if filter data has changed
if config.firstRun { now := time.Now()
return 0
}
config.RLock() config.RLock()
for i := range config.Filters { for i := range config.Filters {
f := &config.Filters[i] // otherwise we will be operating on a copy f := &config.Filters[i] // otherwise we will be operating on a copy
@ -192,7 +215,8 @@ func refreshFiltersIfNecessary(force bool) int {
continue continue
} }
if !force && time.Since(f.LastUpdated) <= updatePeriod { expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60
if !force && expireTime > now.Unix() {
continue continue
} }
@ -214,7 +238,7 @@ func refreshFiltersIfNecessary(force bool) int {
log.Printf("Failed to update filter %s: %s\n", uf.URL, err) log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
continue continue
} }
uf.LastUpdated = time.Now() uf.LastUpdated = now
if updated { if updated {
updateCount++ updateCount++
} }

37
home/filter_test.go Normal file
View File

@ -0,0 +1,37 @@
package home
import (
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestFilters(t *testing.T) {
config.client = &http.Client{
Timeout: time.Minute * 5,
}
f := filter{
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt",
}
// download
ok, err := f.update()
assert.True(t, ok && err == nil)
// refresh
ok, err = f.update()
assert.True(t, !ok && err == nil)
err = f.save()
assert.True(t, err == nil)
err = f.load()
assert.True(t, err == nil)
f.unload()
os.Remove(f.Path())
}

View File

@ -135,22 +135,17 @@ func run(args options) {
config.BindPort = args.bindPort config.BindPort = args.bindPort
} }
loadFilters()
if !config.firstRun { if !config.firstRun {
// Save the updated config // Save the updated config
err := config.write() err := config.write()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
}
// Init the DNS server instance before registering HTTP handlers dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir) initDNSServer(dnsBaseDir)
initDNSServer(dnsBaseDir)
if !config.firstRun { err = startDNSServer()
err := startDNSServer()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -165,13 +160,6 @@ func run(args options) {
config.pidFileName = args.pidFile config.pidFileName = args.pidFile
} }
// Update filters we've just loaded right away, don't wait for periodic update timer
go func() {
refreshFiltersIfNecessary(false)
}()
// Schedule automatic filters updates
go periodicallyRefreshFilters()
// Initialize and run the admin Web interface // Initialize and run the admin Web interface
box := packr.NewBox("../build/static") box := packr.NewBox("../build/static")

View File

@ -35,7 +35,7 @@ type queryLog struct {
lock sync.RWMutex lock sync.RWMutex
} }
// newQueryLog creates a new instance of the query log // create a new instance of the query log
func newQueryLog(conf Config) *queryLog { func newQueryLog(conf Config) *queryLog {
l := queryLog{} l := queryLog{}
l.logFile = filepath.Join(conf.BaseDir, queryLogFileName) l.logFile = filepath.Join(conf.BaseDir, queryLogFileName)
@ -53,7 +53,6 @@ func (l *queryLog) Configure(conf Config) {
l.conf = conf l.conf = conf
} }
// Clear memory buffer and remove the file
func (l *queryLog) Clear() { func (l *queryLog) Clear() {
l.fileFlushLock.Lock() l.fileFlushLock.Lock()
defer l.fileFlushLock.Unlock() defer l.fileFlushLock.Unlock()
@ -164,7 +163,6 @@ func (l *queryLog) Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Res
} }
} }
// getQueryLogJson returns a map with the current query log ready to be converted to a JSON
func (l *queryLog) GetData() []map[string]interface{} { func (l *queryLog) GetData() []map[string]interface{} {
l.lock.RLock() l.lock.RLock()
values := make([]*logEntry, len(l.cache)) values := make([]*logEntry, len(l.cache))

View File

@ -10,14 +10,20 @@ import (
// QueryLog - main interface // QueryLog - main interface
type QueryLog interface { type QueryLog interface {
// Close query log object
Close() Close()
// Set new configuration at runtime // Set new configuration at runtime
// Currently only 'Interval' field is supported. // Currently only 'Interval' field is supported.
Configure(conf Config) Configure(conf Config)
// Add a log entry
Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Result, elapsed time.Duration, addr net.Addr, upstream string) Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Result, elapsed time.Duration, addr net.Addr, upstream string)
// Get log entries
GetData() []map[string]interface{} GetData() []map[string]interface{}
// Clear memory buffer and remove log files
Clear() Clear()
} }
@ -27,7 +33,7 @@ type Config struct {
Interval uint32 // interval to rotate logs (in hours) Interval uint32 // interval to rotate logs (in hours)
} }
// New - create instance // New - create a new instance of the query log
func New(conf Config) QueryLog { func New(conf Config) QueryLog {
return newQueryLog(conf) return newQueryLog(conf)
} }