Pull request: AG-31778-fix-safesearch-https
Squashed commit of the following: commit 85ea3d985e83209e3b49119959aedd330df24d23 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 18 15:19:38 2024 +0200 all: imp docs commit b0695daddbcf191454c5e829ca4d19def8ddacbf Merge: a79f98f2f48c6242a7
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 17 11:06:49 2024 +0200 Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https # Conflicts: # CHANGELOG.md commit a79f98f2f215a4a79ca4d186c0da33db936429dc Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 17 11:05:34 2024 +0200 dnsforward: imp code commit b901a1169cc78313298d70cce770cd1523ccbf9f Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 16 11:03:52 2024 +0200 dnsforward: imp code commit fb6e66971b1b984147ec400ceaff856e7b5710c7 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 16 10:08:51 2024 +0200 all: safesearch rewrites commit 88add21831fff7e04539f5dd299832883a6f3995 Merge: b78ad8f74201ac73cf
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 16 09:43:20 2024 +0200 Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https # Conflicts: # CHANGELOG.md commit b78ad8f748c7fa52533e0541cae16bd51c201370 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Fri Apr 12 13:34:39 2024 +0200 all: safesearch rewrites commit fb3efbb053242c537ca872542006917b8e8ac1ff Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 13:15:37 2024 +0200 safesearch: imp code commit 1193c704f4d30be4a2cc66e84a31c9a6020ab269 Merge: 14e823d7cff7c715c5
Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 13:13:44 2024 +0200 Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https # Conflicts: # CHANGELOG.md commit 14e823d7cc13c275c2ed04704883a94b95e29963 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 13:11:43 2024 +0200 all: safesearch https commit cd403a2897ae56a9059a78f24b104af5805d84ab Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Thu Apr 11 12:09:27 2024 +0200 Revert "all: safesearch https" This reverts commit 1c9564b9b4db70f85b2f827cc06b65d2b67b08b1. commit 1c9564b9b4db70f85b2f827cc06b65d2b67b08b1 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 12:41:47 2024 +0200 all: safesearch https commit 5f42688fbab566973acc8dc414a992819492a9ac Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 09:22:30 2024 +0200 filtering: imp code commit eb9bd9f47cd71cafe8eee4698a8a0d5d25dea3d3 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 09:19:22 2024 +0200 all: changelog commit 0c77c705a942fe83d3809a7efbc8a6baf5886762 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Wed Apr 10 08:55:22 2024 +0200 safesearch: imp tests commit 492a93fbb5ff54678e22a15577f509b2327c2ebe Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 9 14:45:16 2024 +0200 all: changelog commit a665e7246d11503c47d48ccc714e6862f764e930 Author: Dimitry Kolyshev <dkolyshev@adguard.com> Date: Tue Apr 9 14:41:24 2024 +0200 safesearch: https req
This commit is contained in:
parent
48c6242a7b
commit
762ef4a6db
|
@ -27,8 +27,13 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
|
|
||||||
- Support for comments in the ipset file ([#5345]).
|
- Support for comments in the ipset file ([#5345]).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Rewrite rules mechanics was changed due to improve resolving in safe search.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- YouTube restricted mode is not enforced by HTTPS queries on Firefox.
|
||||||
- Support for link-local subnets, i.e. `fe80::/16`, in the access settings
|
- Support for link-local subnets, i.e. `fe80::/16`, in the access settings
|
||||||
([#6192]).
|
([#6192]).
|
||||||
- The ability to apply an invalid configuration for private RDNS, which led to
|
- The ability to apply an invalid configuration for private RDNS, which led to
|
||||||
|
@ -58,7 +63,7 @@ See also the [v0.107.48 GitHub milestone][ms-v0.107.48].
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Access settings not being applied to encrypted protocols ([#6890])
|
- Access settings not being applied to encrypted protocols ([#6890]).
|
||||||
|
|
||||||
[#6890]: https://github.com/AdguardTeam/AdGuardHome/issues/6890
|
[#6890]: https://github.com/AdguardTeam/AdGuardHome/issues/6890
|
||||||
|
|
||||||
|
|
|
@ -64,8 +64,6 @@ type Persistent struct {
|
||||||
// upstream must be used.
|
// upstream must be used.
|
||||||
UpstreamConfig *proxy.CustomUpstreamConfig
|
UpstreamConfig *proxy.CustomUpstreamConfig
|
||||||
|
|
||||||
// TODO(d.kolyshev): Make SafeSearchConf a pointer.
|
|
||||||
SafeSearchConf filtering.SafeSearchConfig
|
|
||||||
SafeSearch filtering.SafeSearch
|
SafeSearch filtering.SafeSearch
|
||||||
|
|
||||||
// BlockedServices is the configuration of blocked services of a client.
|
// BlockedServices is the configuration of blocked services of a client.
|
||||||
|
@ -95,6 +93,9 @@ type Persistent struct {
|
||||||
UseOwnBlockedServices bool
|
UseOwnBlockedServices bool
|
||||||
IgnoreQueryLog bool
|
IgnoreQueryLog bool
|
||||||
IgnoreStatistics bool
|
IgnoreStatistics bool
|
||||||
|
|
||||||
|
// TODO(d.kolyshev): Make SafeSearchConf a pointer.
|
||||||
|
SafeSearchConf filtering.SafeSearchConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTags sets the tags if they are known, otherwise logs an unknown tag.
|
// SetTags sets the tags if they are known, otherwise logs an unknown tag.
|
||||||
|
|
|
@ -2,7 +2,6 @@ package dnsforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
@ -491,19 +490,10 @@ func TestServerRace(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSafeSearch(t *testing.T) {
|
func TestSafeSearch(t *testing.T) {
|
||||||
resolver := &aghtest.Resolver{
|
|
||||||
OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
|
|
||||||
ip4, ip6 := aghtest.HostToIPs(host)
|
|
||||||
|
|
||||||
return []net.IP{ip4.AsSlice(), ip6.AsSlice()}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
safeSearchConf := filtering.SafeSearchConfig{
|
safeSearchConf := filtering.SafeSearchConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Google: true,
|
Google: true,
|
||||||
Yandex: true,
|
Yandex: true,
|
||||||
CustomResolver: resolver,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterConf := &filtering.Config{
|
filterConf := &filtering.Config{
|
||||||
|
@ -540,7 +530,6 @@ func TestSafeSearch(t *testing.T) {
|
||||||
client := &dns.Client{}
|
client := &dns.Client{}
|
||||||
|
|
||||||
yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})
|
yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})
|
||||||
googleIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
host string
|
host string
|
||||||
|
@ -564,19 +553,19 @@ func TestSafeSearch(t *testing.T) {
|
||||||
wantCNAME: "",
|
wantCNAME: "",
|
||||||
}, {
|
}, {
|
||||||
host: "www.google.com.",
|
host: "www.google.com.",
|
||||||
want: googleIP,
|
want: netip.Addr{},
|
||||||
wantCNAME: "forcesafesearch.google.com.",
|
wantCNAME: "forcesafesearch.google.com.",
|
||||||
}, {
|
}, {
|
||||||
host: "www.google.com.af.",
|
host: "www.google.com.af.",
|
||||||
want: googleIP,
|
want: netip.Addr{},
|
||||||
wantCNAME: "forcesafesearch.google.com.",
|
wantCNAME: "forcesafesearch.google.com.",
|
||||||
}, {
|
}, {
|
||||||
host: "www.google.be.",
|
host: "www.google.be.",
|
||||||
want: googleIP,
|
want: netip.Addr{},
|
||||||
wantCNAME: "forcesafesearch.google.com.",
|
wantCNAME: "forcesafesearch.google.com.",
|
||||||
}, {
|
}, {
|
||||||
host: "www.google.by.",
|
host: "www.google.by.",
|
||||||
want: googleIP,
|
want: netip.Addr{},
|
||||||
wantCNAME: "forcesafesearch.google.com.",
|
wantCNAME: "forcesafesearch.google.com.",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@ -593,12 +582,15 @@ func TestSafeSearch(t *testing.T) {
|
||||||
|
|
||||||
cname := testutil.RequireTypeAssert[*dns.CNAME](t, reply.Answer[0])
|
cname := testutil.RequireTypeAssert[*dns.CNAME](t, reply.Answer[0])
|
||||||
assert.Equal(t, tc.wantCNAME, cname.Target)
|
assert.Equal(t, tc.wantCNAME, cname.Target)
|
||||||
|
|
||||||
|
a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[1])
|
||||||
|
assert.NotEmpty(t, a.A)
|
||||||
} else {
|
} else {
|
||||||
require.Len(t, reply.Answer, 1)
|
require.Len(t, reply.Answer, 1)
|
||||||
}
|
|
||||||
|
|
||||||
a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[len(reply.Answer)-1])
|
a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])
|
||||||
assert.Equal(t, net.IP(tc.want.AsSlice()), a.A)
|
assert.Equal(t, net.IP(tc.want.AsSlice()), a.A)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
|
||||||
req := pctx.Req
|
req := pctx.Req
|
||||||
q := req.Question[0]
|
q := req.Question[0]
|
||||||
host := strings.TrimSuffix(q.Name, ".")
|
host := strings.TrimSuffix(q.Name, ".")
|
||||||
|
|
||||||
resVal, err := s.dnsFilter.CheckHost(host, q.Qtype, dctx.setts)
|
resVal, err := s.dnsFilter.CheckHost(host, q.Qtype, dctx.setts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("checking host %q: %w", host, err)
|
return nil, fmt.Errorf("checking host %q: %w", host, err)
|
||||||
|
@ -39,22 +40,15 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
|
||||||
// TODO(a.garipov): Make CheckHost return a pointer.
|
// TODO(a.garipov): Make CheckHost return a pointer.
|
||||||
res = &resVal
|
res = &resVal
|
||||||
switch {
|
switch {
|
||||||
case res.IsFiltered:
|
case isRewrittenCNAME(res):
|
||||||
log.Debug(
|
|
||||||
"dnsforward: host %q is filtered, reason: %q; rule: %q",
|
|
||||||
host,
|
|
||||||
res.Reason,
|
|
||||||
res.Rules[0].Text,
|
|
||||||
)
|
|
||||||
pctx.Res = s.genDNSFilterMessage(pctx, res)
|
|
||||||
case res.Reason.In(filtering.Rewritten, filtering.RewrittenRule) &&
|
|
||||||
res.CanonName != "" &&
|
|
||||||
len(res.IPList) == 0:
|
|
||||||
// Resolve the new canonical name, not the original host name. The
|
// Resolve the new canonical name, not the original host name. The
|
||||||
// original question is readded in processFilteringAfterResponse.
|
// original question is readded in processFilteringAfterResponse.
|
||||||
dctx.origQuestion = q
|
dctx.origQuestion = q
|
||||||
req.Question[0].Name = dns.Fqdn(res.CanonName)
|
req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||||
case res.Reason == filtering.Rewritten:
|
case res.IsFiltered:
|
||||||
|
log.Debug("dnsforward: host %q is filtered, reason: %q", host, res.Reason)
|
||||||
|
pctx.Res = s.genDNSFilterMessage(pctx, res)
|
||||||
|
case res.Reason.In(filtering.Rewritten, filtering.FilteredSafeSearch):
|
||||||
pctx.Res = s.getCNAMEWithIPs(req, res.IPList, res.CanonName)
|
pctx.Res = s.getCNAMEWithIPs(req, res.IPList, res.CanonName)
|
||||||
case res.Reason.In(filtering.RewrittenRule, filtering.RewrittenAutoHosts):
|
case res.Reason.In(filtering.RewrittenRule, filtering.RewrittenAutoHosts):
|
||||||
if err = s.filterDNSRewrite(req, res, pctx); err != nil {
|
if err = s.filterDNSRewrite(req, res, pctx); err != nil {
|
||||||
|
@ -65,6 +59,17 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isRewrittenCNAME returns true if the request considered to be rewritten with
|
||||||
|
// CNAME and has no resolved IPs.
|
||||||
|
func isRewrittenCNAME(res *filtering.Result) (ok bool) {
|
||||||
|
return res.Reason.In(
|
||||||
|
filtering.Rewritten,
|
||||||
|
filtering.RewrittenRule,
|
||||||
|
filtering.FilteredSafeSearch) &&
|
||||||
|
res.CanonName != "" &&
|
||||||
|
len(res.IPList) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// checkHostRules checks the host against filters. It is safe for concurrent
|
// checkHostRules checks the host against filters. It is safe for concurrent
|
||||||
// use.
|
// use.
|
||||||
func (s *Server) checkHostRules(
|
func (s *Server) checkHostRules(
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (s *Server) genDNSFilterMessage(
|
||||||
) (resp *dns.Msg) {
|
) (resp *dns.Msg) {
|
||||||
req := dctx.Req
|
req := dctx.Req
|
||||||
qt := req.Question[0].Qtype
|
qt := req.Question[0].Qtype
|
||||||
if qt != dns.TypeA && qt != dns.TypeAAAA {
|
if qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeHTTPS {
|
||||||
m, _, _ := s.dnsFilter.BlockingMode()
|
m, _, _ := s.dnsFilter.BlockingMode()
|
||||||
if m == filtering.BlockingModeNullIP {
|
if m == filtering.BlockingModeNullIP {
|
||||||
return s.replyCompressed(req)
|
return s.replyCompressed(req)
|
||||||
|
|
|
@ -598,7 +598,8 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
case
|
case
|
||||||
filtering.Rewritten,
|
filtering.Rewritten,
|
||||||
filtering.RewrittenRule:
|
filtering.RewrittenRule,
|
||||||
|
filtering.FilteredSafeSearch:
|
||||||
|
|
||||||
if dctx.origQuestion.Name == "" {
|
if dctx.origQuestion.Name == "" {
|
||||||
// origQuestion is set in case we get only CNAME without IP from
|
// origQuestion is set in case we get only CNAME without IP from
|
||||||
|
@ -608,11 +609,10 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
|
||||||
|
|
||||||
pctx := dctx.proxyCtx
|
pctx := dctx.proxyCtx
|
||||||
pctx.Req.Question[0], pctx.Res.Question[0] = dctx.origQuestion, dctx.origQuestion
|
pctx.Req.Question[0], pctx.Res.Question[0] = dctx.origQuestion, dctx.origQuestion
|
||||||
if len(pctx.Res.Answer) > 0 {
|
|
||||||
rr := s.genAnswerCNAME(pctx.Req, res.CanonName)
|
rr := s.genAnswerCNAME(pctx.Req, res.CanonName)
|
||||||
answer := append([]dns.RR{rr}, pctx.Res.Answer...)
|
answer := append([]dns.RR{rr}, pctx.Res.Answer...)
|
||||||
pctx.Res.Answer = answer
|
pctx.Res.Answer = answer
|
||||||
}
|
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -559,6 +559,8 @@ type Result struct {
|
||||||
Reason Reason `json:",omitempty"`
|
Reason Reason `json:",omitempty"`
|
||||||
|
|
||||||
// IsFiltered is true if the request is filtered.
|
// IsFiltered is true if the request is filtered.
|
||||||
|
//
|
||||||
|
// TODO(d.kolyshev): Get rid of this flag.
|
||||||
IsFiltered bool `json:",omitempty"`
|
IsFiltered bool `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package filtering
|
package filtering
|
||||||
|
|
||||||
import "github.com/miekg/dns"
|
|
||||||
|
|
||||||
// SafeSearch interface describes a service for search engines hosts rewrites.
|
// SafeSearch interface describes a service for search engines hosts rewrites.
|
||||||
type SafeSearch interface {
|
type SafeSearch interface {
|
||||||
// CheckHost checks host with safe search filter. CheckHost must be safe
|
// CheckHost checks host with safe search filter. CheckHost must be safe
|
||||||
|
@ -16,9 +14,6 @@ type SafeSearch interface {
|
||||||
|
|
||||||
// SafeSearchConfig is a struct with safe search related settings.
|
// SafeSearchConfig is a struct with safe search related settings.
|
||||||
type SafeSearchConfig struct {
|
type SafeSearchConfig struct {
|
||||||
// CustomResolver is the resolver used by safe search.
|
|
||||||
CustomResolver Resolver `yaml:"-" json:"-"`
|
|
||||||
|
|
||||||
// Enabled indicates if safe search is enabled entirely.
|
// Enabled indicates if safe search is enabled entirely.
|
||||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||||
|
|
||||||
|
@ -40,13 +35,7 @@ func (d *DNSFilter) checkSafeSearch(
|
||||||
qtype uint16,
|
qtype uint16,
|
||||||
setts *Settings,
|
setts *Settings,
|
||||||
) (res Result, err error) {
|
) (res Result, err error) {
|
||||||
if !setts.ProtectionEnabled ||
|
if d.safeSearch == nil || !setts.ProtectionEnabled || !setts.SafeSearchEnabled {
|
||||||
!setts.SafeSearchEnabled ||
|
|
||||||
(qtype != dns.TypeA && qtype != dns.TypeAAAA) {
|
|
||||||
return Result{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.safeSearch == nil {
|
|
||||||
return Result{}, nil
|
return Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,9 @@ package safesearch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -67,7 +65,6 @@ type Default struct {
|
||||||
engine *urlfilter.DNSEngine
|
engine *urlfilter.DNSEngine
|
||||||
|
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
resolver filtering.Resolver
|
|
||||||
logPrefix string
|
logPrefix string
|
||||||
cacheTTL time.Duration
|
cacheTTL time.Duration
|
||||||
}
|
}
|
||||||
|
@ -80,11 +77,6 @@ func NewDefault(
|
||||||
cacheSize uint,
|
cacheSize uint,
|
||||||
cacheTTL time.Duration,
|
cacheTTL time.Duration,
|
||||||
) (ss *Default, err error) {
|
) (ss *Default, err error) {
|
||||||
var resolver filtering.Resolver = net.DefaultResolver
|
|
||||||
if conf.CustomResolver != nil {
|
|
||||||
resolver = conf.CustomResolver
|
|
||||||
}
|
|
||||||
|
|
||||||
ss = &Default{
|
ss = &Default{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
|
|
||||||
|
@ -92,7 +84,6 @@ func NewDefault(
|
||||||
EnableLRU: true,
|
EnableLRU: true,
|
||||||
MaxSize: cacheSize,
|
MaxSize: cacheSize,
|
||||||
}),
|
}),
|
||||||
resolver: resolver,
|
|
||||||
// Use %s, because the client safe-search names already contain double
|
// Use %s, because the client safe-search names already contain double
|
||||||
// quotes.
|
// quotes.
|
||||||
logPrefix: fmt.Sprintf("safesearch %s: ", name),
|
logPrefix: fmt.Sprintf("safesearch %s: ", name),
|
||||||
|
@ -170,8 +161,11 @@ func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Res
|
||||||
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
|
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
|
switch qtype {
|
||||||
return filtering.Result{}, fmt.Errorf("unsupported question type %s", dns.Type(qtype))
|
case dns.TypeA, dns.TypeAAAA, dns.TypeHTTPS:
|
||||||
|
// Go on.
|
||||||
|
default:
|
||||||
|
return filtering.Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cache. Return cached result if it was found
|
// Check cache. Return cached result if it was found
|
||||||
|
@ -195,6 +189,9 @@ func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Res
|
||||||
}
|
}
|
||||||
|
|
||||||
res = *fltRes
|
res = *fltRes
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider switch back to resolving CNAME records IPs and
|
||||||
|
// saving results to cache.
|
||||||
ss.setCacheResult(host, qtype, res)
|
ss.setCacheResult(host, qtype, res)
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
@ -223,20 +220,13 @@ func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// newResult creates Result object from rewrite rule. qtype must be either
|
// newResult creates Result object from rewrite rule. qtype must be either
|
||||||
// [dns.TypeA] or [dns.TypeAAAA]. If err is nil, res is never nil, so that the
|
// [dns.TypeA] or [dns.TypeAAAA], or [dns.TypeHTTPS]. If err is nil, res is
|
||||||
// empty result is converted into a NODATA response.
|
// never nil, so that the empty result is converted into a NODATA response.
|
||||||
//
|
|
||||||
// TODO(a.garipov): Use the main rewrite result mechanism used in
|
|
||||||
// [dnsforward.Server.filterDNSRequest]. Now we resolve IPs for CNAME to save
|
|
||||||
// them in the safe search cache.
|
|
||||||
func (ss *Default) newResult(
|
func (ss *Default) newResult(
|
||||||
rewrite *rules.DNSRewrite,
|
rewrite *rules.DNSRewrite,
|
||||||
qtype rules.RRType,
|
qtype rules.RRType,
|
||||||
) (res *filtering.Result, err error) {
|
) (res *filtering.Result, err error) {
|
||||||
res = &filtering.Result{
|
res = &filtering.Result{
|
||||||
Rules: []*filtering.ResultRule{{
|
|
||||||
FilterListID: rulelist.URLFilterIDSafeSearch,
|
|
||||||
}},
|
|
||||||
Reason: filtering.FilteredSafeSearch,
|
Reason: filtering.FilteredSafeSearch,
|
||||||
IsFiltered: true,
|
IsFiltered: true,
|
||||||
}
|
}
|
||||||
|
@ -247,69 +237,19 @@ func (ss *Default) newResult(
|
||||||
return nil, fmt.Errorf("expected ip rewrite value, got %T(%[1]v)", rewrite.Value)
|
return nil, fmt.Errorf("expected ip rewrite value, got %T(%[1]v)", rewrite.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Rules[0].IP = ip
|
res.Rules = []*filtering.ResultRule{{
|
||||||
|
FilterListID: rulelist.URLFilterIDSafeSearch,
|
||||||
|
IP: ip,
|
||||||
|
}}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
host := rewrite.NewCNAME
|
res.CanonName = rewrite.NewCNAME
|
||||||
if host == "" {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.CanonName = host
|
|
||||||
|
|
||||||
ss.log(log.DEBUG, "resolving %q", host)
|
|
||||||
|
|
||||||
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("resolving cname: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ss.log(log.DEBUG, "resolved %s", ips)
|
|
||||||
|
|
||||||
for _, ip := range ips {
|
|
||||||
// TODO(a.garipov): Remove this filtering once the resolver we use
|
|
||||||
// actually learns about network.
|
|
||||||
addr := fitToProto(ip, qtype)
|
|
||||||
if addr == (netip.Addr{}) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(e.burkov): Rules[0]?
|
|
||||||
res.Rules[0].IP = addr
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
|
|
||||||
// It panics for other types.
|
|
||||||
func qtypeToProto(qtype rules.RRType) (proto string) {
|
|
||||||
switch qtype {
|
|
||||||
case dns.TypeA:
|
|
||||||
return "ip4"
|
|
||||||
case dns.TypeAAAA:
|
|
||||||
return "ip6"
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("safesearch: unsupported question type %s", dns.Type(qtype)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fitToProto returns a non-nil IP address if ip is the correct protocol version
|
|
||||||
// for qtype. qtype is expected to be either [dns.TypeA] or [dns.TypeAAAA].
|
|
||||||
func fitToProto(ip net.IP, qtype rules.RRType) (res netip.Addr) {
|
|
||||||
if ip4 := ip.To4(); qtype == dns.TypeA {
|
|
||||||
if ip4 != nil {
|
|
||||||
return netip.AddrFrom4([4]byte(ip4))
|
|
||||||
}
|
|
||||||
} else if ip = ip.To16(); ip != nil && qtype == dns.TypeAAAA {
|
|
||||||
return netip.AddrFrom16([16]byte(ip))
|
|
||||||
}
|
|
||||||
|
|
||||||
return netip.Addr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setCacheResult stores data in cache for host. qtype is expected to be either
|
// setCacheResult stores data in cache for host. qtype is expected to be either
|
||||||
// [dns.TypeA] or [dns.TypeAAAA].
|
// [dns.TypeA] or [dns.TypeAAAA].
|
||||||
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
|
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
package safesearch
|
package safesearch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
"github.com/AdguardTeam/urlfilter/rules"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
@ -79,47 +76,6 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
||||||
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSafeSearchCacheGoogle(t *testing.T) {
|
|
||||||
const domain = "www.google.ru"
|
|
||||||
|
|
||||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
|
||||||
|
|
||||||
res, err := ss.CheckHost(domain, testQType)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.False(t, res.IsFiltered)
|
|
||||||
assert.Empty(t, res.Rules)
|
|
||||||
|
|
||||||
resolver := &aghtest.Resolver{
|
|
||||||
OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
|
|
||||||
ip4, ip6 := aghtest.HostToIPs(host)
|
|
||||||
|
|
||||||
return []net.IP{ip4.AsSlice(), ip6.AsSlice()}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ss = newForTest(t, defaultSafeSearchConf)
|
|
||||||
ss.resolver = resolver
|
|
||||||
|
|
||||||
// Lookup for safesearch domain.
|
|
||||||
rewrite := ss.searchHost(domain, testQType)
|
|
||||||
|
|
||||||
wantIP, _ := aghtest.HostToIPs(rewrite.NewCNAME)
|
|
||||||
|
|
||||||
res, err = ss.CheckHost(domain, testQType)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, res.Rules, 1)
|
|
||||||
|
|
||||||
assert.Equal(t, wantIP, res.Rules[0].IP)
|
|
||||||
|
|
||||||
// Check cache.
|
|
||||||
cachedValue, isFound := ss.getCachedResult(domain, testQType)
|
|
||||||
require.True(t, isFound)
|
|
||||||
require.Len(t, cachedValue.Rules, 1)
|
|
||||||
|
|
||||||
assert.Equal(t, wantIP, cachedValue.Rules[0].IP)
|
|
||||||
}
|
|
||||||
|
|
||||||
const googleHost = "www.google.com"
|
const googleHost = "www.google.com"
|
||||||
|
|
||||||
var dnsRewriteSink *rules.DNSRewrite
|
var dnsRewriteSink *rules.DNSRewrite
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||||
|
@ -31,8 +30,6 @@ const (
|
||||||
|
|
||||||
// testConf is the default safe search configuration for tests.
|
// testConf is the default safe search configuration for tests.
|
||||||
var testConf = filtering.SafeSearchConfig{
|
var testConf = filtering.SafeSearchConfig{
|
||||||
CustomResolver: nil,
|
|
||||||
|
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|
||||||
Bing: true,
|
Bing: true,
|
||||||
|
@ -52,61 +49,60 @@ func TestDefault_CheckHost_yandex(t *testing.T) {
|
||||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Check host for each domain.
|
hosts := []string{
|
||||||
for _, host := range []string{
|
|
||||||
"yandex.ru",
|
"yandex.ru",
|
||||||
"yAndeX.ru",
|
"yAndeX.ru",
|
||||||
"YANdex.COM",
|
"YANdex.COM",
|
||||||
"yandex.by",
|
"yandex.by",
|
||||||
"yandex.kz",
|
"yandex.kz",
|
||||||
"www.yandex.com",
|
"www.yandex.com",
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want netip.Addr
|
||||||
|
name string
|
||||||
|
qt uint16
|
||||||
|
}{{
|
||||||
|
want: yandexIP,
|
||||||
|
name: "a",
|
||||||
|
qt: dns.TypeA,
|
||||||
|
}, {
|
||||||
|
want: netip.Addr{},
|
||||||
|
name: "aaaa",
|
||||||
|
qt: dns.TypeAAAA,
|
||||||
|
}, {
|
||||||
|
want: netip.Addr{},
|
||||||
|
name: "https",
|
||||||
|
qt: dns.TypeHTTPS,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
for _, host := range hosts {
|
||||||
|
// Check host for each domain.
|
||||||
var res filtering.Result
|
var res filtering.Result
|
||||||
res, err = ss.CheckHost(host, testQType)
|
res, err = ss.CheckHost(host, tc.qt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, res.IsFiltered)
|
assert.True(t, res.IsFiltered)
|
||||||
|
assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
|
||||||
|
|
||||||
|
if tc.want == (netip.Addr{}) {
|
||||||
|
assert.Empty(t, res.Rules)
|
||||||
|
} else {
|
||||||
require.Len(t, res.Rules, 1)
|
require.Len(t, res.Rules, 1)
|
||||||
|
|
||||||
assert.Equal(t, yandexIP, res.Rules[0].IP)
|
rule := res.Rules[0]
|
||||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
assert.Equal(t, tc.want, rule.IP)
|
||||||
|
assert.Equal(t, rulelist.URLFilterIDSafeSearch, rule.FilterListID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
func TestDefault_CheckHost_yandexAAAA(t *testing.T) {
|
}
|
||||||
conf := testConf
|
|
||||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
res, err := ss.CheckHost("www.yandex.ru", dns.TypeAAAA)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.True(t, res.IsFiltered)
|
|
||||||
|
|
||||||
// TODO(a.garipov): Currently, the safe-search filter returns a single rule
|
|
||||||
// with a nil IP address. This isn't really necessary and should be changed
|
|
||||||
// once the TODO in [safesearch.Default.newResult] is resolved.
|
|
||||||
require.Len(t, res.Rules, 1)
|
|
||||||
|
|
||||||
assert.Empty(t, res.Rules[0].IP)
|
|
||||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefault_CheckHost_google(t *testing.T) {
|
func TestDefault_CheckHost_google(t *testing.T) {
|
||||||
resolver := &aghtest.Resolver{
|
ss, err := safesearch.NewDefault(testConf, "", testCacheSize, testCacheTTL)
|
||||||
OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
|
|
||||||
ip4, ip6 := aghtest.HostToIPs(host)
|
|
||||||
|
|
||||||
return []net.IP{ip4.AsSlice(), ip6.AsSlice()}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
wantIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
|
|
||||||
|
|
||||||
conf := testConf
|
|
||||||
conf.CustomResolver = resolver
|
|
||||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Check host for each domain.
|
// Check host for each domain.
|
||||||
|
@ -125,11 +121,9 @@ func TestDefault_CheckHost_google(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, res.IsFiltered)
|
assert.True(t, res.IsFiltered)
|
||||||
|
assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
|
||||||
require.Len(t, res.Rules, 1)
|
assert.Equal(t, "forcesafesearch.google.com", res.CanonName)
|
||||||
|
assert.Empty(t, res.Rules)
|
||||||
assert.Equal(t, wantIP, res.Rules[0].IP)
|
|
||||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,17 +148,7 @@ func (r *testResolver) LookupIP(
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
|
func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
|
||||||
conf := testConf
|
ss, err := safesearch.NewDefault(testConf, "", testCacheSize, testCacheTTL)
|
||||||
conf.CustomResolver = &testResolver{
|
|
||||||
OnLookupIP: func(_ context.Context, network, host string) (ips []net.IP, err error) {
|
|
||||||
assert.Equal(t, "ip6", network)
|
|
||||||
assert.Equal(t, "safe.duckduckgo.com", host)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// The DuckDuckGo safe-search addresses are resolved through CNAMEs, but
|
// The DuckDuckGo safe-search addresses are resolved through CNAMEs, but
|
||||||
|
@ -174,14 +158,9 @@ func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, res.IsFiltered)
|
assert.True(t, res.IsFiltered)
|
||||||
|
assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
|
||||||
// TODO(a.garipov): Currently, the safe-search filter returns a single rule
|
assert.Equal(t, "safe.duckduckgo.com", res.CanonName)
|
||||||
// with a nil IP address. This isn't really necessary and should be changed
|
assert.Empty(t, res.Rules)
|
||||||
// once the TODO in [safesearch.Default.newResult] is resolved.
|
|
||||||
require.Len(t, res.Rules, 1)
|
|
||||||
|
|
||||||
assert.Empty(t, res.Rules[0].IP)
|
|
||||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefault_Update(t *testing.T) {
|
func TestDefault_Update(t *testing.T) {
|
||||||
|
|
|
@ -249,8 +249,6 @@ func (o *clientObject) toPersistent(
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.SafeSearchConf.Enabled {
|
if o.SafeSearchConf.Enabled {
|
||||||
o.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
|
||||||
|
|
||||||
err = cli.SetSafeSearch(
|
err = cli.SetSafeSearch(
|
||||||
o.SafeSearchConf,
|
o.SafeSearchConf,
|
||||||
filteringConf.SafeSearchCacheSize,
|
filteringConf.SafeSearchCacheSize,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package home
|
package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
@ -528,36 +527,6 @@ func closeDNSServer() {
|
||||||
log.Debug("all dns modules are closed")
|
log.Debug("all dns modules are closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.Addr with IPv4 and IPv6 instances.
|
|
||||||
func (r safeSearchResolver) LookupIP(
|
|
||||||
ctx context.Context,
|
|
||||||
network string,
|
|
||||||
host string,
|
|
||||||
) (ips []net.IP, err error) {
|
|
||||||
addrs, err := Context.dnsServer.Resolve(ctx, network, 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.AsSlice())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkStatsAndQuerylogDirs checks and returns directory paths to store
|
// checkStatsAndQuerylogDirs checks and returns directory paths to store
|
||||||
// statistics and query log.
|
// statistics and query log.
|
||||||
func checkStatsAndQuerylogDirs(
|
func checkStatsAndQuerylogDirs(
|
||||||
|
|
|
@ -439,7 +439,6 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
|
||||||
conf.ParentalBlockHost = host
|
conf.ParentalBlockHost = host
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
|
||||||
conf.SafeSearch, err = safesearch.NewDefault(
|
conf.SafeSearch, err = safesearch.NewDefault(
|
||||||
conf.SafeSearchConf,
|
conf.SafeSearchConf,
|
||||||
"default",
|
"default",
|
||||||
|
|
Loading…
Reference in New Issue