Pull request: 1163 safesearch vol.2

Merge in DNS/adguard-home from 1163-safesearch-1-2-1 to master

Squashed commit of the following:

commit d3a5ebef35210019842145074e898129b42f1f2c
Merge: b85264ae c6706445
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 15 09:17:53 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-2-1

    # Conflicts:
    #	CHANGELOG.md

commit b85264aefc5f191ac6cb194b519f03ba15829a4e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Mar 14 00:16:07 2023 +0700

    home: imp code

commit ac2ed7a5ce8db40628e7d4d1c8634641e5f38b0b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 23:02:06 2023 +0700

    all: changelog

commit f0fccafcb01f50c7051df53bbe9b02cab75aa71e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 22:42:36 2023 +0700

    all: changelog

commit 37df29bf6372939644fb28e3d70365496e0cb4f6
Merge: b227b277 595484e0
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 22:38:57 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-2-1

commit b227b2775b4866d69241ad87acf99700715552cb
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 16:56:01 2023 +0700

    all: imp docs

commit 6fd39fc3565c3f4bc7a7113d17733c20dfe24d8d
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 16:55:03 2023 +0700

    home: imp code

commit 3bb3bb7c7dcf97b2a5602a7d2b6770c08b4d863d
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 12:16:53 2023 +0700

    home: imp docs

commit 5f573a56a9fd9942ad677fa0fae6b24228dab653
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 11:56:47 2023 +0700

    home: imp code

commit 23eeb5552cf2510596b2311cc3eda53ac678ffcc
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 10 10:57:33 2023 +0700

    home: imp code

commit 643de2fca1b5917c61fe83e1e472222404f3cd21
Merge: dada6e63 a2053526
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 9 21:03:08 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1163-safesearch-1-2-1

commit dada6e63ca5324d30775e2da1727da891743f654
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 9 17:09:03 2023 +0700

    all: imp docs

commit 81a180d99dd9a995440d5f4e2ebca34678e7d0c7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 9 15:12:43 2023 +0700

    all: imp code

commit fa84877bc777004d246d71d0a9ae0bd9ee568a91
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 9 10:53:05 2023 +0700

    all: imp code

commit 6d7e02e745d72921a693d4f09eec7ce21c2aefd4
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 9 10:40:02 2023 +0700

    all: imp docs

commit 0a4332997070fb8d2fb3a34d32b92f57a325ff06
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Mar 7 22:00:52 2023 +0700

    safesearch: fix merge

commit 145c2222ba4cf7f8909b816d83829d2217c94243
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 2 11:41:48 2023 +0700

    safesearch: fix merge

commit 14c6a8005fe15b5d5a39f91b17c96d8670975811
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 1 12:50:09 2023 +0700

    all: docs

commit 2a85c8831866bf1c34c423a289461fc1e32667b5
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Mar 1 12:47:00 2023 +0700

    all: use safesearch package
This commit is contained in:
Dimitry Kolyshev 2023-03-15 14:31:07 +03:00
parent c6706445c9
commit 2b5e4850d0
15 changed files with 507 additions and 605 deletions

View file

@ -23,11 +23,48 @@ See also the [v0.107.27 GitHub milestone][ms-v0.107.27].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Added
- The ability to manage safesearch for each service by using the new
`safe_search` field ([#1163]).
### Changed
#### Configuration Changes
In this release, the schema version has changed from 17 to 19.
- The `dns.safesearch_enabled` field has been replaced with `safe_search`
object containing per-service settings.
- The `clients.persistent.safesearch_enabled` field has been replaced with
`safe_search` object containing per-service settings.
```yaml
# BEFORE:
'safesearch_enabled': true
# AFTER:
'safe_search':
'enabled': true
'bing': true
'duckduckgo': true
'google': true
'pixabay': true
'yandex': true
'youtube': true
```
To rollback this change, move the value of `dns.safe_search.enabled` into the
`dns.safesearch_enabled`, then remove `dns.safe_search` field. Do the same
client's specific `clients.persistent.safesearch` and then change the
`schema_version` back to `17`.
### Fixed ### Fixed
- Panic caused by empty top-level domain name label in `/etc/hosts` files - Panic caused by empty top-level domain name label in `/etc/hosts` files
([#5584]). ([#5584]).
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
[#5584]: https://github.com/AdguardTeam/AdGuardHome/issues/5584 [#5584]: https://github.com/AdguardTeam/AdGuardHome/issues/5584
<!-- <!--

View file

@ -23,6 +23,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
@ -412,7 +413,7 @@ func TestServerRace(t *testing.T) {
filterConf := &filtering.Config{ filterConf := &filtering.Config{
SafeBrowsingEnabled: true, SafeBrowsingEnabled: true,
SafeBrowsingCacheSize: 1000, SafeBrowsingCacheSize: 1000,
SafeSearchEnabled: true, SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
SafeSearchCacheSize: 1000, SafeSearchCacheSize: 1000,
ParentalCacheSize: 1000, ParentalCacheSize: 1000,
CacheTime: 30, CacheTime: 30,
@ -440,12 +441,26 @@ func TestServerRace(t *testing.T) {
func TestSafeSearch(t *testing.T) { func TestSafeSearch(t *testing.T) {
resolver := &aghtest.TestResolver{} resolver := &aghtest.TestResolver{}
filterConf := &filtering.Config{ safeSearchConf := filtering.SafeSearchConfig{
SafeSearchEnabled: true, Enabled: true,
SafeSearchCacheSize: 1000, Google: true,
CacheTime: 30, Yandex: true,
CustomResolver: resolver, CustomResolver: resolver,
} }
filterConf := &filtering.Config{
SafeSearchConf: safeSearchConf,
SafeSearchCacheSize: 1000,
CacheTime: 30,
}
safeSearch, err := safesearch.NewDefaultSafeSearch(
safeSearchConf,
filterConf.SafeSearchCacheSize,
time.Minute*time.Duration(filterConf.CacheTime),
)
require.NoError(t, err)
filterConf.SafeSearch = safeSearch
forwardConf := ServerConfig{ forwardConf := ServerConfig{
UDPListenAddrs: []*net.UDPAddr{{}}, UDPListenAddrs: []*net.UDPAddr{{}},
TCPListenAddrs: []*net.TCPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}},
@ -498,7 +513,8 @@ func TestSafeSearch(t *testing.T) {
t.Run(tc.host, func(t *testing.T) { t.Run(tc.host, func(t *testing.T) {
req := createTestMessage(tc.host) req := createTestMessage(tc.host)
reply, _, err := client.Exchange(req, addr) var reply *dns.Msg
reply, _, err = client.Exchange(req, addr)
require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err) require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
assertResponse(t, reply, tc.want) assertResponse(t, reply, tc.want)
}) })

View file

@ -57,7 +57,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
filterConf := &filtering.Config{ filterConf := &filtering.Config{
SafeBrowsingEnabled: true, SafeBrowsingEnabled: true,
SafeBrowsingCacheSize: 1000, SafeBrowsingCacheSize: 1000,
SafeSearchEnabled: true, SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
SafeSearchCacheSize: 1000, SafeSearchCacheSize: 1000,
ParentalCacheSize: 1000, ParentalCacheSize: 1000,
CacheTime: 30, CacheTime: 30,
@ -133,7 +133,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
filterConf := &filtering.Config{ filterConf := &filtering.Config{
SafeBrowsingEnabled: true, SafeBrowsingEnabled: true,
SafeBrowsingCacheSize: 1000, SafeBrowsingCacheSize: 1000,
SafeSearchEnabled: true, SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
SafeSearchCacheSize: 1000, SafeSearchCacheSize: 1000,
ParentalCacheSize: 1000, ParentalCacheSize: 1000,
CacheTime: 30, CacheTime: 30,

View file

@ -63,6 +63,9 @@ type Settings struct {
SafeSearchEnabled bool SafeSearchEnabled bool
SafeBrowsingEnabled bool SafeBrowsingEnabled bool
ParentalEnabled bool ParentalEnabled bool
// ClientSafeSearch is a client configured safe search.
ClientSafeSearch SafeSearch
} }
// Resolver is the interface for net.Resolver to simplify testing. // Resolver is the interface for net.Resolver to simplify testing.
@ -83,14 +86,17 @@ type Config struct {
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours) FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
ParentalEnabled bool `yaml:"parental_enabled"` ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"` SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
SafeBrowsingCacheSize uint `yaml:"safebrowsing_cache_size"` // (in bytes) SafeBrowsingCacheSize uint `yaml:"safebrowsing_cache_size"` // (in bytes)
SafeSearchCacheSize uint `yaml:"safesearch_cache_size"` // (in bytes) SafeSearchCacheSize uint `yaml:"safesearch_cache_size"` // (in bytes)
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes) ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
// TODO(a.garipov): Use timeutil.Duration
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes) CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
SafeSearchConf SafeSearchConfig `yaml:"safe_search"`
SafeSearch SafeSearch `yaml:"-"`
Rewrites []*LegacyRewrite `yaml:"rewrites"` Rewrites []*LegacyRewrite `yaml:"rewrites"`
// Names of services to block (globally). // Names of services to block (globally).
@ -107,9 +113,6 @@ type Config struct {
// Register an HTTP handler // Register an HTTP handler
HTTPRegister aghhttp.RegisterFunc `yaml:"-"` HTTPRegister aghhttp.RegisterFunc `yaml:"-"`
// CustomResolver is the resolver used by DNSFilter.
CustomResolver Resolver `yaml:"-"`
// HTTPClient is the client to use for updating the remote filters. // HTTPClient is the client to use for updating the remote filters.
HTTPClient *http.Client `yaml:"-"` HTTPClient *http.Client `yaml:"-"`
@ -172,7 +175,6 @@ type DNSFilter struct {
safebrowsingCache cache.Cache safebrowsingCache cache.Cache
parentalCache cache.Cache parentalCache cache.Cache
safeSearchCache cache.Cache
Config // for direct access by library users, even a = assignment Config // for direct access by library users, even a = assignment
// confLock protects Config. // confLock protects Config.
@ -182,11 +184,6 @@ type DNSFilter struct {
filtersInitializerChan chan filtersInitializerParams filtersInitializerChan chan filtersInitializerParams
filtersInitializerLock sync.Mutex filtersInitializerLock sync.Mutex
// resolver only looks up the IP address of the host while safe search.
//
// TODO(e.burkov): Use upstream that configured in dnsforward instead.
resolver Resolver
refreshLock *sync.Mutex refreshLock *sync.Mutex
// filterTitleRegexp is the regular expression to retrieve a name of a // filterTitleRegexp is the regular expression to retrieve a name of a
@ -195,6 +192,7 @@ type DNSFilter struct {
// TODO(e.burkov): Don't use regexp for such a simple text processing task. // TODO(e.burkov): Don't use regexp for such a simple text processing task.
filterTitleRegexp *regexp.Regexp filterTitleRegexp *regexp.Regexp
safeSearch SafeSearch
hostCheckers []hostChecker hostCheckers []hostChecker
} }
@ -298,7 +296,7 @@ func (d *DNSFilter) GetConfig() (s Settings) {
return Settings{ return Settings{
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0, FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0,
SafeSearchEnabled: d.Config.SafeSearchEnabled, SafeSearchEnabled: d.Config.SafeSearchConf.Enabled,
SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled, SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled,
ParentalEnabled: d.Config.ParentalEnabled, ParentalEnabled: d.Config.ParentalEnabled,
} }
@ -942,7 +940,6 @@ func InitModule() {
// be non-nil. // be non-nil.
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) { func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d = &DNSFilter{ d = &DNSFilter{
resolver: net.DefaultResolver,
refreshLock: &sync.Mutex{}, refreshLock: &sync.Mutex{},
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`), filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
} }
@ -951,18 +948,12 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
EnableLRU: true, EnableLRU: true,
MaxSize: c.SafeBrowsingCacheSize, MaxSize: c.SafeBrowsingCacheSize,
}) })
d.safeSearchCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.SafeSearchCacheSize,
})
d.parentalCache = cache.New(cache.Config{ d.parentalCache = cache.New(cache.Config{
EnableLRU: true, EnableLRU: true,
MaxSize: c.ParentalCacheSize, MaxSize: c.ParentalCacheSize,
}) })
if r := c.CustomResolver; r != nil { d.safeSearch = c.SafeSearch
d.resolver = r
}
d.hostCheckers = []hostChecker{{ d.hostCheckers = []hostChecker{{
check: d.matchSysHosts, check: d.matchSysHosts,

View file

@ -2,10 +2,8 @@ package filtering
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"net" "net"
"strings"
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
@ -33,7 +31,6 @@ func purgeCaches(d *DNSFilter) {
for _, c := range []cache.Cache{ for _, c := range []cache.Cache{
d.safebrowsingCache, d.safebrowsingCache,
d.parentalCache, d.parentalCache,
d.safeSearchCache,
} { } {
if c != nil { if c != nil {
c.Clear() c.Clear()
@ -51,7 +48,7 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
c.ParentalCacheSize = 10000 c.ParentalCacheSize = 10000
c.SafeSearchCacheSize = 1000 c.SafeSearchCacheSize = 1000
c.CacheTime = 30 c.CacheTime = 30
setts.SafeSearchEnabled = c.SafeSearchEnabled setts.SafeSearchEnabled = c.SafeSearchConf.Enabled
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
setts.ParentalEnabled = c.ParentalEnabled setts.ParentalEnabled = c.ParentalEnabled
} else { } else {
@ -216,164 +213,6 @@ func TestParallelSB(t *testing.T) {
}) })
} }
// Safe Search.
func TestSafeSearch(t *testing.T) {
d, _ := newForTest(t, &Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
val, ok := d.SafeSearchDomain("www.google.com")
require.True(t, ok)
assert.Equal(t, "forcesafesearch.google.com", val)
}
func TestCheckHostSafeSearchYandex(t *testing.T) {
d, setts := newForTest(t, &Config{
SafeSearchEnabled: true,
}, nil)
t.Cleanup(d.Close)
yandexIP := net.IPv4(213, 180, 193, 56)
// Check host for each domain.
for _, host := range []string{
"yAndeX.ru",
"YANdex.COM",
"yandex.ua",
"yandex.by",
"yandex.kz",
"www.yandex.com",
} {
t.Run(strings.ToLower(host), func(t *testing.T) {
res, err := d.CheckHost(host, dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, yandexIP, res.Rules[0].IP)
assert.EqualValues(t, SafeSearchListID, res.Rules[0].FilterListID)
})
}
}
func TestCheckHostSafeSearchGoogle(t *testing.T) {
resolver := &aghtest.TestResolver{}
d, setts := newForTest(t, &Config{
SafeSearchEnabled: true,
CustomResolver: resolver,
}, nil)
t.Cleanup(d.Close)
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
// Check host for each domain.
for _, host := range []string{
"www.google.com",
"www.google.im",
"www.google.co.in",
"www.google.iq",
"www.google.is",
"www.google.it",
"www.google.je",
} {
t.Run(host, func(t *testing.T) {
res, err := d.CheckHost(host, dns.TypeA, setts)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
assert.Equal(t, ip, res.Rules[0].IP)
assert.EqualValues(t, SafeSearchListID, res.Rules[0].FilterListID)
})
}
}
func TestSafeSearchCacheYandex(t *testing.T) {
d, setts := newForTest(t, nil, nil)
t.Cleanup(d.Close)
const domain = "yandex.ru"
// Check host with disabled safesearch.
res, err := d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
require.Empty(t, res.Rules)
yandexIP := net.IPv4(213, 180, 193, 56)
d, setts = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
res, err = d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
// For yandex we already know valid IP.
require.Len(t, res.Rules, 1)
assert.Equal(t, res.Rules[0].IP, yandexIP)
// Check cache.
cachedValue, isFound := getCachedResult(d.safeSearchCache, domain)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
}
func TestSafeSearchCacheGoogle(t *testing.T) {
resolver := &aghtest.TestResolver{}
d, setts := newForTest(t, &Config{
CustomResolver: resolver,
}, nil)
t.Cleanup(d.Close)
const domain = "www.google.ru"
res, err := d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
assert.False(t, res.IsFiltered)
require.Empty(t, res.Rules)
d, setts = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
t.Cleanup(d.Close)
d.resolver = resolver
// Lookup for safesearch domain.
safeDomain, ok := d.SafeSearchDomain(domain)
require.True(t, ok)
ips, err := resolver.LookupIP(context.Background(), "ip", safeDomain)
require.NoError(t, err)
var ip net.IP
for _, foundIP := range ips {
if foundIP.To4() != nil {
ip = foundIP
break
}
}
res, err = d.CheckHost(domain, dns.TypeA, setts)
require.NoError(t, err)
require.Len(t, res.Rules, 1)
assert.True(t, res.Rules[0].IP.Equal(ip))
// Check cache.
cachedValue, isFound := getCachedResult(d.safeSearchCache, domain)
require.True(t, isFound)
require.Len(t, cachedValue.Rules, 1)
assert.True(t, cachedValue.Rules[0].IP.Equal(ip))
}
// Parental. // Parental.
func TestParentalControl(t *testing.T) { func TestParentalControl(t *testing.T) {
@ -854,27 +693,3 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
} }
}) })
} }
func BenchmarkSafeSearch(b *testing.B) {
d, _ := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
b.Cleanup(d.Close)
for n := 0; n < b.N; n++ {
val, ok := d.SafeSearchDomain("www.google.com")
require.True(b, ok)
assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
}
}
func BenchmarkSafeSearchParallel(b *testing.B) {
d, _ := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
b.Cleanup(d.Close)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
val, ok := d.SafeSearchDomain("www.google.com")
require.True(b, ok)
assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
}
})
}

View file

@ -1,19 +1,8 @@
package filtering package filtering
import ( import (
"bytes"
"context"
"encoding/binary"
"encoding/gob"
"fmt"
"net"
"net/http"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
) )
// SafeSearch interface describes a service for search engines hosts rewrites. // SafeSearch interface describes a service for search engines hosts rewrites.
@ -44,60 +33,8 @@ type SafeSearchConfig struct {
YouTube bool `yaml:"youtube" json:"youtube"` YouTube bool `yaml:"youtube" json:"youtube"`
} }
/* // checkSafeSearch checks host with safe search engine. Matches
expire byte[4] // [hostChecker.check].
res Result
*/
func (d *DNSFilter) setCacheResult(cache cache.Cache, host string, res Result) int {
var buf bytes.Buffer
expire := uint(time.Now().Unix()) + d.Config.CacheTime*60
exp := make([]byte, 4)
binary.BigEndian.PutUint32(exp, uint32(expire))
_, _ = buf.Write(exp)
enc := gob.NewEncoder(&buf)
err := enc.Encode(res)
if err != nil {
log.Error("gob.Encode(): %s", err)
return 0
}
val := buf.Bytes()
_ = cache.Set([]byte(host), val)
return len(val)
}
func getCachedResult(cache cache.Cache, host string) (Result, bool) {
data := cache.Get([]byte(host))
if data == nil {
return Result{}, false
}
exp := int(binary.BigEndian.Uint32(data[:4]))
if exp <= int(time.Now().Unix()) {
cache.Del([]byte(host))
return Result{}, false
}
var buf bytes.Buffer
buf.Write(data[4:])
dec := gob.NewDecoder(&buf)
r := Result{}
err := dec.Decode(&r)
if err != nil {
log.Debug("gob.Decode(): %s", err)
return Result{}, false
}
return r, true
}
// SafeSearchDomain returns replacement address for search engine
func (d *DNSFilter) SafeSearchDomain(host string) (string, bool) {
val, ok := safeSearchDomains[host]
return val, ok
}
func (d *DNSFilter) checkSafeSearch( func (d *DNSFilter) checkSafeSearch(
host string, host string,
_ uint16, _ uint16,
@ -107,295 +44,14 @@ func (d *DNSFilter) checkSafeSearch(
return Result{}, nil return Result{}, nil
} }
if log.GetLevel() >= log.DEBUG { if d.safeSearch == nil {
timer := log.StartTimer()
defer timer.LogElapsed("SafeSearch: lookup for %s", host)
}
// Check cache. Return cached result if it was found
cachedValue, isFound := getCachedResult(d.safeSearchCache, host)
if isFound {
// atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1)
log.Tracef("SafeSearch: found in cache: %s", host)
return cachedValue, nil
}
safeHost, ok := d.SafeSearchDomain(host)
if !ok {
return Result{}, nil return Result{}, nil
} }
res = Result{ clientSafeSearch := setts.ClientSafeSearch
Rules: []*ResultRule{{ if clientSafeSearch != nil {
FilterListID: SafeSearchListID, return clientSafeSearch.CheckHost(host, dns.TypeA)
}},
Reason: FilteredSafeSearch,
IsFiltered: true,
} }
if ip := net.ParseIP(safeHost); ip != nil { return d.safeSearch.CheckHost(host, dns.TypeA)
res.Rules[0].IP = ip
valLen := d.setCacheResult(d.safeSearchCache, host, res)
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen)
return res, nil
}
ips, err := d.resolver.LookupIP(context.Background(), "ip", safeHost)
if err != nil {
log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err)
return Result{}, err
}
for _, ip := range ips {
if ip = ip.To4(); ip == nil {
continue
}
res.Rules[0].IP = ip
l := d.setCacheResult(d.safeSearchCache, host, res)
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, l)
return res, nil
}
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
}
func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeSearchEnabled, true)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeSearchEnabled, false)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.SafeSearchEnabled),
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
var safeSearchDomains = map[string]string{
"yandex.com": "213.180.193.56",
"yandex.ru": "213.180.193.56",
"yandex.ua": "213.180.193.56",
"yandex.by": "213.180.193.56",
"yandex.kz": "213.180.193.56",
"www.yandex.com": "213.180.193.56",
"www.yandex.ru": "213.180.193.56",
"www.yandex.ua": "213.180.193.56",
"www.yandex.by": "213.180.193.56",
"www.yandex.kz": "213.180.193.56",
"www.bing.com": "strict.bing.com",
"duckduckgo.com": "safe.duckduckgo.com",
"www.duckduckgo.com": "safe.duckduckgo.com",
"start.duckduckgo.com": "safe.duckduckgo.com",
"www.google.com": "forcesafesearch.google.com",
"www.google.ad": "forcesafesearch.google.com",
"www.google.ae": "forcesafesearch.google.com",
"www.google.com.af": "forcesafesearch.google.com",
"www.google.com.ag": "forcesafesearch.google.com",
"www.google.com.ai": "forcesafesearch.google.com",
"www.google.al": "forcesafesearch.google.com",
"www.google.am": "forcesafesearch.google.com",
"www.google.co.ao": "forcesafesearch.google.com",
"www.google.com.ar": "forcesafesearch.google.com",
"www.google.as": "forcesafesearch.google.com",
"www.google.at": "forcesafesearch.google.com",
"www.google.com.au": "forcesafesearch.google.com",
"www.google.az": "forcesafesearch.google.com",
"www.google.ba": "forcesafesearch.google.com",
"www.google.com.bd": "forcesafesearch.google.com",
"www.google.be": "forcesafesearch.google.com",
"www.google.bf": "forcesafesearch.google.com",
"www.google.bg": "forcesafesearch.google.com",
"www.google.com.bh": "forcesafesearch.google.com",
"www.google.bi": "forcesafesearch.google.com",
"www.google.bj": "forcesafesearch.google.com",
"www.google.com.bn": "forcesafesearch.google.com",
"www.google.com.bo": "forcesafesearch.google.com",
"www.google.com.br": "forcesafesearch.google.com",
"www.google.bs": "forcesafesearch.google.com",
"www.google.bt": "forcesafesearch.google.com",
"www.google.co.bw": "forcesafesearch.google.com",
"www.google.by": "forcesafesearch.google.com",
"www.google.com.bz": "forcesafesearch.google.com",
"www.google.ca": "forcesafesearch.google.com",
"www.google.cd": "forcesafesearch.google.com",
"www.google.cf": "forcesafesearch.google.com",
"www.google.cg": "forcesafesearch.google.com",
"www.google.ch": "forcesafesearch.google.com",
"www.google.ci": "forcesafesearch.google.com",
"www.google.co.ck": "forcesafesearch.google.com",
"www.google.cl": "forcesafesearch.google.com",
"www.google.cm": "forcesafesearch.google.com",
"www.google.cn": "forcesafesearch.google.com",
"www.google.com.co": "forcesafesearch.google.com",
"www.google.co.cr": "forcesafesearch.google.com",
"www.google.com.cu": "forcesafesearch.google.com",
"www.google.cv": "forcesafesearch.google.com",
"www.google.com.cy": "forcesafesearch.google.com",
"www.google.cz": "forcesafesearch.google.com",
"www.google.de": "forcesafesearch.google.com",
"www.google.dj": "forcesafesearch.google.com",
"www.google.dk": "forcesafesearch.google.com",
"www.google.dm": "forcesafesearch.google.com",
"www.google.com.do": "forcesafesearch.google.com",
"www.google.dz": "forcesafesearch.google.com",
"www.google.com.ec": "forcesafesearch.google.com",
"www.google.ee": "forcesafesearch.google.com",
"www.google.com.eg": "forcesafesearch.google.com",
"www.google.es": "forcesafesearch.google.com",
"www.google.com.et": "forcesafesearch.google.com",
"www.google.fi": "forcesafesearch.google.com",
"www.google.com.fj": "forcesafesearch.google.com",
"www.google.fm": "forcesafesearch.google.com",
"www.google.fr": "forcesafesearch.google.com",
"www.google.ga": "forcesafesearch.google.com",
"www.google.ge": "forcesafesearch.google.com",
"www.google.gg": "forcesafesearch.google.com",
"www.google.com.gh": "forcesafesearch.google.com",
"www.google.com.gi": "forcesafesearch.google.com",
"www.google.gl": "forcesafesearch.google.com",
"www.google.gm": "forcesafesearch.google.com",
"www.google.gp": "forcesafesearch.google.com",
"www.google.gr": "forcesafesearch.google.com",
"www.google.com.gt": "forcesafesearch.google.com",
"www.google.gy": "forcesafesearch.google.com",
"www.google.com.hk": "forcesafesearch.google.com",
"www.google.hn": "forcesafesearch.google.com",
"www.google.hr": "forcesafesearch.google.com",
"www.google.ht": "forcesafesearch.google.com",
"www.google.hu": "forcesafesearch.google.com",
"www.google.co.id": "forcesafesearch.google.com",
"www.google.ie": "forcesafesearch.google.com",
"www.google.co.il": "forcesafesearch.google.com",
"www.google.im": "forcesafesearch.google.com",
"www.google.co.in": "forcesafesearch.google.com",
"www.google.iq": "forcesafesearch.google.com",
"www.google.is": "forcesafesearch.google.com",
"www.google.it": "forcesafesearch.google.com",
"www.google.je": "forcesafesearch.google.com",
"www.google.com.jm": "forcesafesearch.google.com",
"www.google.jo": "forcesafesearch.google.com",
"www.google.co.jp": "forcesafesearch.google.com",
"www.google.co.ke": "forcesafesearch.google.com",
"www.google.com.kh": "forcesafesearch.google.com",
"www.google.ki": "forcesafesearch.google.com",
"www.google.kg": "forcesafesearch.google.com",
"www.google.co.kr": "forcesafesearch.google.com",
"www.google.com.kw": "forcesafesearch.google.com",
"www.google.kz": "forcesafesearch.google.com",
"www.google.la": "forcesafesearch.google.com",
"www.google.com.lb": "forcesafesearch.google.com",
"www.google.li": "forcesafesearch.google.com",
"www.google.lk": "forcesafesearch.google.com",
"www.google.co.ls": "forcesafesearch.google.com",
"www.google.lt": "forcesafesearch.google.com",
"www.google.lu": "forcesafesearch.google.com",
"www.google.lv": "forcesafesearch.google.com",
"www.google.com.ly": "forcesafesearch.google.com",
"www.google.co.ma": "forcesafesearch.google.com",
"www.google.md": "forcesafesearch.google.com",
"www.google.me": "forcesafesearch.google.com",
"www.google.mg": "forcesafesearch.google.com",
"www.google.mk": "forcesafesearch.google.com",
"www.google.ml": "forcesafesearch.google.com",
"www.google.com.mm": "forcesafesearch.google.com",
"www.google.mn": "forcesafesearch.google.com",
"www.google.ms": "forcesafesearch.google.com",
"www.google.com.mt": "forcesafesearch.google.com",
"www.google.mu": "forcesafesearch.google.com",
"www.google.mv": "forcesafesearch.google.com",
"www.google.mw": "forcesafesearch.google.com",
"www.google.com.mx": "forcesafesearch.google.com",
"www.google.com.my": "forcesafesearch.google.com",
"www.google.co.mz": "forcesafesearch.google.com",
"www.google.com.na": "forcesafesearch.google.com",
"www.google.com.nf": "forcesafesearch.google.com",
"www.google.com.ng": "forcesafesearch.google.com",
"www.google.com.ni": "forcesafesearch.google.com",
"www.google.ne": "forcesafesearch.google.com",
"www.google.nl": "forcesafesearch.google.com",
"www.google.no": "forcesafesearch.google.com",
"www.google.com.np": "forcesafesearch.google.com",
"www.google.nr": "forcesafesearch.google.com",
"www.google.nu": "forcesafesearch.google.com",
"www.google.co.nz": "forcesafesearch.google.com",
"www.google.com.om": "forcesafesearch.google.com",
"www.google.com.pa": "forcesafesearch.google.com",
"www.google.com.pe": "forcesafesearch.google.com",
"www.google.com.pg": "forcesafesearch.google.com",
"www.google.com.ph": "forcesafesearch.google.com",
"www.google.com.pk": "forcesafesearch.google.com",
"www.google.pl": "forcesafesearch.google.com",
"www.google.pn": "forcesafesearch.google.com",
"www.google.com.pr": "forcesafesearch.google.com",
"www.google.ps": "forcesafesearch.google.com",
"www.google.pt": "forcesafesearch.google.com",
"www.google.com.py": "forcesafesearch.google.com",
"www.google.com.qa": "forcesafesearch.google.com",
"www.google.ro": "forcesafesearch.google.com",
"www.google.ru": "forcesafesearch.google.com",
"www.google.rw": "forcesafesearch.google.com",
"www.google.com.sa": "forcesafesearch.google.com",
"www.google.com.sb": "forcesafesearch.google.com",
"www.google.sc": "forcesafesearch.google.com",
"www.google.se": "forcesafesearch.google.com",
"www.google.com.sg": "forcesafesearch.google.com",
"www.google.sh": "forcesafesearch.google.com",
"www.google.si": "forcesafesearch.google.com",
"www.google.sk": "forcesafesearch.google.com",
"www.google.com.sl": "forcesafesearch.google.com",
"www.google.sn": "forcesafesearch.google.com",
"www.google.so": "forcesafesearch.google.com",
"www.google.sm": "forcesafesearch.google.com",
"www.google.sr": "forcesafesearch.google.com",
"www.google.st": "forcesafesearch.google.com",
"www.google.com.sv": "forcesafesearch.google.com",
"www.google.td": "forcesafesearch.google.com",
"www.google.tg": "forcesafesearch.google.com",
"www.google.co.th": "forcesafesearch.google.com",
"www.google.com.tj": "forcesafesearch.google.com",
"www.google.tk": "forcesafesearch.google.com",
"www.google.tl": "forcesafesearch.google.com",
"www.google.tm": "forcesafesearch.google.com",
"www.google.tn": "forcesafesearch.google.com",
"www.google.to": "forcesafesearch.google.com",
"www.google.com.tr": "forcesafesearch.google.com",
"www.google.tt": "forcesafesearch.google.com",
"www.google.com.tw": "forcesafesearch.google.com",
"www.google.co.tz": "forcesafesearch.google.com",
"www.google.com.ua": "forcesafesearch.google.com",
"www.google.co.ug": "forcesafesearch.google.com",
"www.google.co.uk": "forcesafesearch.google.com",
"www.google.com.uy": "forcesafesearch.google.com",
"www.google.co.uz": "forcesafesearch.google.com",
"www.google.com.vc": "forcesafesearch.google.com",
"www.google.co.ve": "forcesafesearch.google.com",
"www.google.vg": "forcesafesearch.google.com",
"www.google.co.vi": "forcesafesearch.google.com",
"www.google.com.vn": "forcesafesearch.google.com",
"www.google.vu": "forcesafesearch.google.com",
"www.google.ws": "forcesafesearch.google.com",
"www.google.rs": "forcesafesearch.google.com",
"www.youtube.com": "restrictmoderate.youtube.com",
"m.youtube.com": "restrictmoderate.youtube.com",
"youtubei.googleapis.com": "restrictmoderate.youtube.com",
"youtube.googleapis.com": "restrictmoderate.youtube.com",
"www.youtube-nocookie.com": "restrictmoderate.youtube.com",
"pixabay.com": "safesearch.pixabay.com",
} }

View file

@ -0,0 +1,29 @@
package filtering
import (
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
)
// TODO(d.kolyshev): Replace handlers below with the new API.
func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, true)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, false)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled),
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}

View file

@ -4,6 +4,7 @@ import (
"encoding" "encoding"
"fmt" "fmt"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
) )
@ -15,6 +16,9 @@ type Client struct {
// these upstream must be used. // these upstream must be used.
upstreamConfig *proxy.UpstreamConfig upstreamConfig *proxy.UpstreamConfig
safeSearchConf filtering.SafeSearchConfig
SafeSearch filtering.SafeSearch
Name string Name string
IDs []string IDs []string
@ -24,7 +28,6 @@ type Client struct {
UseOwnSettings bool UseOwnSettings bool
FilteringEnabled bool FilteringEnabled bool
SafeSearchEnabled bool
SafeBrowsingEnabled bool SafeBrowsingEnabled bool
ParentalEnabled bool ParentalEnabled bool
UseOwnBlockedServices bool UseOwnBlockedServices bool

View file

@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
@ -69,6 +70,7 @@ func (clients *clientsContainer) Init(
dhcpServer dhcpd.Interface, dhcpServer dhcpd.Interface,
etcHosts *aghnet.HostsContainer, etcHosts *aghnet.HostsContainer,
arpdb aghnet.ARPDB, arpdb aghnet.ARPDB,
filteringConf *filtering.Config,
) { ) {
if clients.list != nil { if clients.list != nil {
log.Fatal("clients.list != nil") log.Fatal("clients.list != nil")
@ -82,7 +84,7 @@ func (clients *clientsContainer) Init(
clients.dhcpServer = dhcpServer clients.dhcpServer = dhcpServer
clients.etcHosts = etcHosts clients.etcHosts = etcHosts
clients.arpdb = arpdb clients.arpdb = arpdb
clients.addFromConfig(objects) clients.addFromConfig(objects, filteringConf)
if clients.testing { if clients.testing {
return return
@ -133,6 +135,8 @@ func (clients *clientsContainer) reloadARP() {
// clientObject is the YAML representation of a persistent client. // clientObject is the YAML representation of a persistent client.
type clientObject struct { type clientObject struct {
SafeSearchConf filtering.SafeSearchConfig `yaml:"safe_search"`
Name string `yaml:"name"` Name string `yaml:"name"`
Tags []string `yaml:"tags"` Tags []string `yaml:"tags"`
@ -143,14 +147,13 @@ type clientObject struct {
UseGlobalSettings bool `yaml:"use_global_settings"` UseGlobalSettings bool `yaml:"use_global_settings"`
FilteringEnabled bool `yaml:"filtering_enabled"` FilteringEnabled bool `yaml:"filtering_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"` ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"` SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
} }
// addFromConfig initializes the clients container with objects from the // addFromConfig initializes the clients container with objects from the
// configuration file. // configuration file.
func (clients *clientsContainer) addFromConfig(objects []*clientObject) { func (clients *clientsContainer) addFromConfig(objects []*clientObject, filteringConf *filtering.Config) {
for _, o := range objects { for _, o := range objects {
cli := &Client{ cli := &Client{
Name: o.Name, Name: o.Name,
@ -161,11 +164,28 @@ func (clients *clientsContainer) addFromConfig(objects []*clientObject) {
UseOwnSettings: !o.UseGlobalSettings, UseOwnSettings: !o.UseGlobalSettings,
FilteringEnabled: o.FilteringEnabled, FilteringEnabled: o.FilteringEnabled,
ParentalEnabled: o.ParentalEnabled, ParentalEnabled: o.ParentalEnabled,
SafeSearchEnabled: o.SafeSearchEnabled, safeSearchConf: o.SafeSearchConf,
SafeBrowsingEnabled: o.SafeBrowsingEnabled, SafeBrowsingEnabled: o.SafeBrowsingEnabled,
UseOwnBlockedServices: !o.UseGlobalBlockedServices, UseOwnBlockedServices: !o.UseGlobalBlockedServices,
} }
if o.SafeSearchConf.Enabled {
o.SafeSearchConf.CustomResolver = safeSearchResolver{}
ss, err := safesearch.NewDefaultSafeSearch(
o.SafeSearchConf,
filteringConf.SafeSearchCacheSize,
time.Minute*time.Duration(filteringConf.CacheTime),
)
if err != nil {
log.Error("clients: init client safesearch %s: %s", cli.Name, err)
continue
}
cli.SafeSearch = ss
}
for _, s := range o.BlockedServices { for _, s := range o.BlockedServices {
if filtering.BlockedSvcKnown(s) { if filtering.BlockedSvcKnown(s) {
cli.BlockedServices = append(cli.BlockedServices, s) cli.BlockedServices = append(cli.BlockedServices, s)
@ -210,7 +230,7 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
UseGlobalSettings: !cli.UseOwnSettings, UseGlobalSettings: !cli.UseOwnSettings,
FilteringEnabled: cli.FilteringEnabled, FilteringEnabled: cli.FilteringEnabled,
ParentalEnabled: cli.ParentalEnabled, ParentalEnabled: cli.ParentalEnabled,
SafeSearchEnabled: cli.SafeSearchEnabled, SafeSearchConf: cli.safeSearchConf,
SafeBrowsingEnabled: cli.SafeBrowsingEnabled, SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
UseGlobalBlockedServices: !cli.UseOwnBlockedServices, UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
} }

View file

@ -19,7 +19,7 @@ func TestClients(t *testing.T) {
clients := clientsContainer{} clients := clientsContainer{}
clients.testing = true clients.testing = true
clients.Init(nil, nil, nil, nil) clients.Init(nil, nil, nil, nil, nil)
t.Run("add_success", func(t *testing.T) { t.Run("add_success", func(t *testing.T) {
var ( var (
@ -201,7 +201,7 @@ func TestClientsWHOIS(t *testing.T) {
clients := clientsContainer{ clients := clientsContainer{
testing: true, testing: true,
} }
clients.Init(nil, nil, nil, nil) clients.Init(nil, nil, nil, nil, nil)
whois := &RuntimeClientWHOISInfo{ whois := &RuntimeClientWHOISInfo{
Country: "AU", Country: "AU",
Orgname: "Example Org", Orgname: "Example Org",
@ -250,7 +250,7 @@ func TestClientsAddExisting(t *testing.T) {
clients := clientsContainer{ clients := clientsContainer{
testing: true, testing: true,
} }
clients.Init(nil, nil, nil, nil) clients.Init(nil, nil, nil, nil, nil)
t.Run("simple", func(t *testing.T) { t.Run("simple", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1") ip := netip.MustParseAddr("1.1.1.1")
@ -328,7 +328,7 @@ func TestClientsCustomUpstream(t *testing.T) {
clients := clientsContainer{ clients := clientsContainer{
testing: true, testing: true,
} }
clients.Init(nil, nil, nil, nil) clients.Init(nil, nil, nil, nil, nil)
// Add client with upstreams. // Add client with upstreams.
ok, err := clients.Add(&Client{ ok, err := clients.Add(&Client{

View file

@ -7,6 +7,7 @@ import (
"net/netip" "net/netip"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
) )
// clientJSON is a common structure used by several handlers to deal with // clientJSON is a common structure used by several handlers to deal with
@ -38,6 +39,7 @@ type clientJSON struct {
FilteringEnabled bool `json:"filtering_enabled"` FilteringEnabled bool `json:"filtering_enabled"`
ParentalEnabled bool `json:"parental_enabled"` ParentalEnabled bool `json:"parental_enabled"`
SafeBrowsingEnabled bool `json:"safebrowsing_enabled"` SafeBrowsingEnabled bool `json:"safebrowsing_enabled"`
// Deprecated: use safeSearchConf.
SafeSearchEnabled bool `json:"safesearch_enabled"` SafeSearchEnabled bool `json:"safesearch_enabled"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"` UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
UseGlobalSettings bool `json:"use_global_settings"` UseGlobalSettings bool `json:"use_global_settings"`
@ -88,6 +90,20 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
// Convert JSON object to Client object // Convert JSON object to Client object
func jsonToClient(cj clientJSON) (c *Client) { func jsonToClient(cj clientJSON) (c *Client) {
// TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field.
safeSearchConf := filtering.SafeSearchConfig{Enabled: cj.SafeSearchEnabled}
// Set default service flags for enabled safesearch.
if safeSearchConf.Enabled {
safeSearchConf.Bing = true
safeSearchConf.DuckDuckGo = true
safeSearchConf.Google = true
safeSearchConf.Pixabay = true
safeSearchConf.Yandex = true
safeSearchConf.YouTube = true
}
return &Client{ return &Client{
Name: cj.Name, Name: cj.Name,
IDs: cj.IDs, IDs: cj.IDs,
@ -95,7 +111,7 @@ func jsonToClient(cj clientJSON) (c *Client) {
UseOwnSettings: !cj.UseGlobalSettings, UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled, FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled, ParentalEnabled: cj.ParentalEnabled,
SafeSearchEnabled: cj.SafeSearchEnabled, safeSearchConf: safeSearchConf,
SafeBrowsingEnabled: cj.SafeBrowsingEnabled, SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
UseOwnBlockedServices: !cj.UseGlobalBlockedServices, UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
@ -107,6 +123,11 @@ func jsonToClient(cj clientJSON) (c *Client) {
// Convert Client object to JSON // Convert Client object to JSON
func clientToJSON(c *Client) (cj *clientJSON) { func clientToJSON(c *Client) (cj *clientJSON) {
// TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field.
cloneVal := c.safeSearchConf
safeSearchConf := &cloneVal
return &clientJSON{ return &clientJSON{
Name: c.Name, Name: c.Name,
IDs: c.IDs, IDs: c.IDs,
@ -114,7 +135,7 @@ func clientToJSON(c *Client) (cj *clientJSON) {
UseGlobalSettings: !c.UseOwnSettings, UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled, FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled, ParentalEnabled: c.ParentalEnabled,
SafeSearchEnabled: c.SafeSearchEnabled, SafeSearchEnabled: safeSearchConf.Enabled,
SafeBrowsingEnabled: c.SafeBrowsingEnabled, SafeBrowsingEnabled: c.SafeBrowsingEnabled,
UseGlobalBlockedServices: !c.UseOwnBlockedServices, UseGlobalBlockedServices: !c.UseOwnBlockedServices,

View file

@ -1,6 +1,7 @@
package home package home
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@ -426,7 +427,8 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
} }
setts.FilteringEnabled = c.FilteringEnabled setts.FilteringEnabled = c.FilteringEnabled
setts.SafeSearchEnabled = c.SafeSearchEnabled setts.SafeSearchEnabled = c.safeSearchConf.Enabled
setts.ClientSafeSearch = c.SafeSearch
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
setts.ParentalEnabled = c.ParentalEnabled setts.ParentalEnabled = c.ParentalEnabled
} }
@ -556,3 +558,29 @@ func nonDupEmptyHostNames(list []string) (set *stringutil.Set, err error) {
return set, nil return set, nil
} }
// safeSearchResolver is a [filtering.Resolver] implementation used for safe
// search.
type safeSearchResolver struct{}
// type check
var _ filtering.Resolver = safeSearchResolver{}
// LookupIP implements [filtering.Resolver] interface for safeSearchResolver.
// It returns the slice of net.IP with IPv4 and IPv6 instances.
func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) {
addrs, err := Context.dnsServer.Resolve(host)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, fmt.Errorf("couldn't lookup host: %s", host)
}
for _, a := range addrs {
ips = append(ips, a.IP)
}
return ips, nil
}

View file

@ -28,6 +28,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/AdGuardHome/internal/updater"
@ -298,6 +299,16 @@ func setupConfig(opts options) (err error) {
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules) config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
config.DNS.DnsfilterConf.HTTPClient = Context.client config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{}
config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefaultSafeSearch(
config.DNS.DnsfilterConf.SafeSearchConf,
config.DNS.DnsfilterConf.SafeSearchCacheSize,
time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime),
)
if err != nil {
return fmt.Errorf("initializing safesearch: %w", err)
}
config.DHCP.WorkDir = Context.workDir config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified config.DHCP.ConfigModified = onConfigModified
@ -328,11 +339,33 @@ func setupConfig(opts options) (err error) {
arpdb = aghnet.NewARPDB() arpdb = aghnet.NewARPDB()
} }
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb) Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb, config.DNS.DnsfilterConf)
if opts.bindPort != 0 { if opts.bindPort != 0 {
config.BindPort = opts.bindPort
err = checkPorts()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
}
// override bind host/port from the console
if opts.bindHost.IsValid() {
config.BindHost = opts.bindHost
}
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile
}
return nil
}
// checkPorts is a helper for ports validation in config.
func checkPorts() (err error) {
tcpPorts := aghalg.UniqChecker[tcpPort]{} tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(tcpPorts, tcpPort(opts.bindPort)) addPorts(tcpPorts, tcpPort(config.BindPort))
udpPorts := aghalg.UniqChecker[udpPort]{} udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(config.DNS.Port)) addPorts(udpPorts, udpPort(config.DNS.Port))
@ -354,17 +387,6 @@ func setupConfig(opts options) (err error) {
return fmt.Errorf("validating udp ports: %w", err) return fmt.Errorf("validating udp ports: %w", err)
} }
config.BindPort = opts.bindPort
}
// override bind host/port from the console
if opts.bindHost.IsValid() {
config.BindHost = opts.bindHost
}
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile
}
return nil return nil
} }

View file

@ -22,7 +22,7 @@ import (
) )
// currentSchemaVersion is the current schema version. // currentSchemaVersion is the current schema version.
const currentSchemaVersion = 17 const currentSchemaVersion = 19
// These aliases are provided for convenience. // These aliases are provided for convenience.
type ( type (
@ -90,6 +90,8 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
upgradeSchema14to15, upgradeSchema14to15,
upgradeSchema15to16, upgradeSchema15to16,
upgradeSchema16to17, upgradeSchema16to17,
upgradeSchema17to18,
upgradeSchema18to19,
} }
n := 0 n := 0
@ -943,6 +945,125 @@ func upgradeSchema16to17(diskConf yobj) (err error) {
return nil return nil
} }
// upgradeSchema17to18 performs the following changes:
//
// # BEFORE:
// 'dns':
// 'safesearch_enabled': true
//
// # AFTER:
// 'dns':
// 'safe_search':
// 'enabled': true
// 'bing': true
// 'duckduckgo': true
// 'google': true
// 'pixabay': true
// 'yandex': true
// 'youtube': true
func upgradeSchema17to18(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 17 to 18")
diskConf["schema_version"] = 18
dnsVal, ok := diskConf["dns"]
if !ok {
return nil
}
dns, ok := dnsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
}
safeSearch := yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
}
const safeSearchKey = "safesearch_enabled"
v, has := dns[safeSearchKey]
if has {
safeSearch["enabled"] = v
}
delete(dns, safeSearchKey)
dns["safe_search"] = safeSearch
return nil
}
// upgradeSchema18to19 performs the following changes:
//
// # BEFORE:
// 'clients':
// 'persistent':
// - 'name': 'client-name'
// 'safesearch_enabled': true
//
// # AFTER:
// 'clients':
// 'persistent':
// - 'name': 'client-name'
// 'safe_search':
// 'enabled': true
// 'bing': true
// 'duckduckgo': true
// 'google': true
// 'pixabay': true
// 'yandex': true
// 'youtube': true
func upgradeSchema18to19(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 18 to 19")
diskConf["schema_version"] = 19
clientsVal, ok := diskConf["clients"]
if !ok {
return nil
}
clients, ok := clientsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of clients: %T", clientsVal)
}
persistent, ok := clients["persistent"].([]yobj)
if !ok {
return nil
}
const safeSearchKey = "safesearch_enabled"
for i := range persistent {
c := persistent[i]
safeSearch := yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
}
v, has := c[safeSearchKey]
if has {
safeSearch["enabled"] = v
}
delete(c, safeSearchKey)
c["safe_search"] = safeSearch
}
return nil
}
// TODO(a.garipov): Replace with log.Output when we port it to our logging // TODO(a.garipov): Replace with log.Output when we port it to our logging
// package. // package.
func funcName() string { func funcName() string {

View file

@ -808,3 +808,146 @@ func TestUpgradeSchema16to17(t *testing.T) {
}) })
} }
} }
func TestUpgradeSchema17to18(t *testing.T) {
const newSchemaVer = 18
defaultWantObj := yobj{
"dns": yobj{
"safe_search": yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
},
"schema_version": newSchemaVer,
}
testCases := []struct {
in yobj
want yobj
name string
}{{
in: yobj{"dns": yobj{}},
want: defaultWantObj,
name: "default_values",
}, {
in: yobj{"dns": yobj{"safesearch_enabled": true}},
want: defaultWantObj,
name: "enabled",
}, {
in: yobj{"dns": yobj{"safesearch_enabled": false}},
want: yobj{
"dns": yobj{
"safe_search": map[string]any{
"enabled": false,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
},
"schema_version": newSchemaVer,
},
name: "disabled",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema17to18(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}
func TestUpgradeSchema18to19(t *testing.T) {
const newSchemaVer = 19
defaultWantObj := yobj{
"clients": yobj{
"persistent": []yobj{{
"name": "localhost",
"safe_search": yobj{
"enabled": true,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
}},
},
"schema_version": newSchemaVer,
}
testCases := []struct {
in yobj
want yobj
name string
}{{
in: yobj{
"clients": yobj{},
},
want: yobj{
"clients": yobj{},
"schema_version": newSchemaVer,
},
name: "no_clients",
}, {
in: yobj{
"clients": yobj{
"persistent": []yobj{{"name": "localhost"}},
},
},
want: defaultWantObj,
name: "default_values",
}, {
in: yobj{
"clients": yobj{
"persistent": []yobj{{"name": "localhost", "safesearch_enabled": true}},
},
},
want: defaultWantObj,
name: "enabled",
}, {
in: yobj{
"clients": yobj{
"persistent": []yobj{{"name": "localhost", "safesearch_enabled": false}},
},
},
want: yobj{
"clients": yobj{"persistent": []yobj{{
"name": "localhost",
"safe_search": yobj{
"enabled": false,
"bing": true,
"duckduckgo": true,
"google": true,
"pixabay": true,
"yandex": true,
"youtube": true,
},
}}},
"schema_version": newSchemaVer,
},
name: "disabled",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema18to19(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}