Compare commits
9 Commits
7164d8b4d7
...
a9de638afb
Author | SHA1 | Date |
---|---|---|
VasiliyMooduckovich | a9de638afb | |
Ainar Garipov | b9d5e5ba0f | |
Stanislav Chzhen | c1ee2c7e5e | |
Ainar Garipov | 856cc40cf3 | |
Dimitry Kolyshev | 0cff3dbcda | |
Dimitry Kolyshev | 60f48e2d00 | |
Eugene Burkov | f85d048315 | |
Dimitry Kolyshev | 762ef4a6db | |
VasiliyMooduckovich | e9c9e72701 |
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -27,8 +27,18 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||
|
||||
- Support for comments in the ipset file ([#5345]).
|
||||
|
||||
### Changed
|
||||
|
||||
- Private rDNS resolution now also affects `SOA` and `NS` requests ([#6882]).
|
||||
- Rewrite rules mechanics was changed due to improve resolving in safe search.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support for link-local subnets, i.e. `fe80::/16`, as client identifiers
|
||||
([#6312]).
|
||||
- Issues with QUIC and HTTP/3 upstreams on older Linux kernel versions
|
||||
([#6422]).
|
||||
- YouTube restricted mode is not enforced by HTTPS queries on Firefox.
|
||||
- Support for link-local subnets, i.e. `fe80::/16`, in the access settings
|
||||
([#6192]).
|
||||
- The ability to apply an invalid configuration for private RDNS, which led to
|
||||
|
@ -43,8 +53,11 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||
[#5345]: https://github.com/AdguardTeam/AdGuardHome/issues/5345
|
||||
[#5812]: https://github.com/AdguardTeam/AdGuardHome/issues/5812
|
||||
[#6192]: https://github.com/AdguardTeam/AdGuardHome/issues/6192
|
||||
[#6312]: https://github.com/AdguardTeam/AdGuardHome/issues/6312
|
||||
[#6422]: https://github.com/AdguardTeam/AdGuardHome/issues/6422
|
||||
[#6854]: https://github.com/AdguardTeam/AdGuardHome/issues/6854
|
||||
[#6875]: https://github.com/AdguardTeam/AdGuardHome/issues/6875
|
||||
[#6882]: https://github.com/AdguardTeam/AdGuardHome/issues/6882
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
|
@ -58,7 +71,7 @@ See also the [v0.107.48 GitHub milestone][ms-v0.107.48].
|
|||
|
||||
### 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
|
||||
|
||||
|
|
|
@ -13,14 +13,14 @@
|
|||
"fallback_dns_desc": "List of fallback DNS servers used when upstream DNS servers are not responding. The syntax is the same as in the main upstreams field above.",
|
||||
"fallback_dns_placeholder": "Enter one fallback DNS server per line",
|
||||
"local_ptr_title": "Private reverse DNS servers",
|
||||
"local_ptr_desc": "The DNS servers that AdGuard Home uses for local PTR queries. These servers are used to resolve PTR requests for addresses in private IP ranges, for example \"192.168.12.34\", using reverse DNS. If not set, AdGuard Home uses the addresses of the default DNS resolvers of your OS except for the addresses of AdGuard Home itself.",
|
||||
"local_ptr_desc": "DNS servers used by AdGuard Home for private PTR, SOA, and NS requests. A request is considered private if it asks for an ARPA domain containing a subnet within private IP ranges (such as \"192.168.12.34\") and comes from a client with a private IP address. If not set, the default DNS resolvers of your OS will be used, except for the AdGuard Home IP addresses.",
|
||||
"local_ptr_default_resolver": "By default, AdGuard Home uses the following reverse DNS resolvers: {{ip}}.",
|
||||
"local_ptr_no_default_resolver": "AdGuard Home could not determine suitable private reverse DNS resolvers for this system.",
|
||||
"local_ptr_placeholder": "Enter one IP address per line",
|
||||
"resolve_clients_title": "Enable reverse resolving of clients' IP addresses",
|
||||
"resolve_clients_desc": "Reversely resolve clients' IP addresses into their hostnames by sending PTR queries to corresponding resolvers (private DNS servers for local clients, upstream servers for clients with public IP addresses).",
|
||||
"use_private_ptr_resolvers_title": "Use private reverse DNS resolvers",
|
||||
"use_private_ptr_resolvers_desc": "Perform reverse DNS lookups for locally served addresses using these upstream servers. If disabled, AdGuard Home responds with NXDOMAIN to all such PTR requests except for clients known from DHCP, /etc/hosts, and so on.",
|
||||
"use_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, etc. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN.",
|
||||
"check_dhcp_servers": "Check for DHCP servers",
|
||||
"save_config": "Save configuration",
|
||||
"enabled_dhcp": "DHCP server enabled",
|
||||
|
@ -211,7 +211,7 @@
|
|||
"form_error_url_format": "Invalid URL format",
|
||||
"form_error_url_or_path_format": "Invalid URL or absolute path of the list",
|
||||
"custom_filter_rules": "Custom filtering rules",
|
||||
"custom_filter_rules_hint": "Enter one rule on a line. You can use either adblock rules or hosts files syntax.",
|
||||
"custom_filter_rules_hint": "Enter one rule per line. You can use either adblock rules or hosts files syntax.",
|
||||
"system_host_files": "System hosts files",
|
||||
"examples_title": "Examples",
|
||||
"example_meaning_filter_block": "block access to example.org and all its subdomains;",
|
||||
|
|
6
go.mod
6
go.mod
|
@ -3,7 +3,8 @@ module github.com/AdguardTeam/AdGuardHome
|
|||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.69.2
|
||||
// TODO(a.garipov): Use a tagged version once released.
|
||||
github.com/AdguardTeam/dnsproxy v0.70.1-0.20240424112457-69feed2dd25e
|
||||
github.com/AdguardTeam/golibs v0.23.2
|
||||
github.com/AdguardTeam/urlfilter v0.18.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
|
@ -28,7 +29,8 @@ require (
|
|||
// own code for that. Perhaps, use gopacket.
|
||||
github.com/mdlayher/raw v0.1.0
|
||||
github.com/miekg/dns v1.1.58
|
||||
github.com/quic-go/quic-go v0.42.0
|
||||
// TODO(a.garipov): Use a tagged version once released.
|
||||
github.com/quic-go/quic-go v0.42.1-0.20240424132812-713525777535
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ti-mo/netfilter v0.5.1
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
|
|
8
go.sum
8
go.sum
|
@ -1,5 +1,5 @@
|
|||
github.com/AdguardTeam/dnsproxy v0.69.2 h1:/qnjEILMIM7koAIcy+ZB19lb+PSZjJWKjxuGyqVVpp0=
|
||||
github.com/AdguardTeam/dnsproxy v0.69.2/go.mod h1:zpA9eBxakSyjKC/bUac+UPSYTp/Q43aOmNlBV2/D6ug=
|
||||
github.com/AdguardTeam/dnsproxy v0.70.1-0.20240424112457-69feed2dd25e h1:ju0wprmCakjAOIuvKrmLU+hUFiStIsreWVk4JWmQPm0=
|
||||
github.com/AdguardTeam/dnsproxy v0.70.1-0.20240424112457-69feed2dd25e/go.mod h1:eWyFj9zVMdJ4tjHULulfFIXiu6GID/aVvewLVVXLXWE=
|
||||
github.com/AdguardTeam/golibs v0.23.2 h1:rMjYantwtQ39e8G4zBQ6ZLlm4s3XH30Bc9VxhoOHwao=
|
||||
github.com/AdguardTeam/golibs v0.23.2/go.mod h1:o9i55Sx6v7qogRQeqaBfmLbC/pZqeMBWi015U5PTDY0=
|
||||
github.com/AdguardTeam/urlfilter v0.18.0 h1:ZZzwODC/ADpjJSODxySrrUnt/fvOCfGFaCW6j+wsGfQ=
|
||||
|
@ -101,8 +101,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
|
|||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
|
||||
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
||||
github.com/quic-go/quic-go v0.42.1-0.20240424132812-713525777535 h1:63/XLGwhqZUU0L4DPWihJH9tyqwmvfaxNPjU9I/Av/M=
|
||||
github.com/quic-go/quic-go v0.42.1-0.20240424132812-713525777535/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
|
|
|
@ -197,8 +197,10 @@ func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
|||
return ci.uidToClient[uid], true
|
||||
}
|
||||
|
||||
ipWithoutZone := ip.WithZone("")
|
||||
ci.subnetToUID.Range(func(pref netip.Prefix, id UID) (cont bool) {
|
||||
if pref.Contains(ip) {
|
||||
// Remove zone before checking because prefixes strip zones.
|
||||
if pref.Contains(ipWithoutZone) {
|
||||
uid, found = id, true
|
||||
|
||||
return false
|
||||
|
@ -214,6 +216,26 @@ func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
// FindByIPWithoutZone finds a persistent client by IP address without zone. It
|
||||
// strips the IPv6 zone index from the stored IP addresses before comparing,
|
||||
// because querylog entries don't have it. See TODO on [querylog.logEntry.IP].
|
||||
//
|
||||
// Note that multiple clients can have the same IP address with different zones.
|
||||
// Therefore, the result of this method is indeterminate.
|
||||
func (ci *Index) FindByIPWithoutZone(ip netip.Addr) (c *Persistent) {
|
||||
if (ip == netip.Addr{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for addr, uid := range ci.ipToUID {
|
||||
if addr.WithZone("") == ip {
|
||||
return ci.uidToClient[uid]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// find finds persistent client by MAC.
|
||||
func (ci *Index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
|
||||
k := macToKey(mac)
|
||||
|
|
|
@ -35,27 +35,49 @@ func TestClientIndex(t *testing.T) {
|
|||
|
||||
cliID = "client-id"
|
||||
cliMAC = "11:11:11:11:11:11"
|
||||
|
||||
linkLocalIP = "fe80::abcd:abcd:abcd:ab%eth0"
|
||||
linkLocalSubnet = "fe80::/16"
|
||||
)
|
||||
|
||||
clients := []*Persistent{{
|
||||
Name: "client1",
|
||||
IPs: []netip.Addr{
|
||||
netip.MustParseAddr(cliIP1),
|
||||
netip.MustParseAddr(cliIPv6),
|
||||
},
|
||||
}, {
|
||||
Name: "client2",
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
|
||||
}, {
|
||||
Name: "client_with_mac",
|
||||
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
|
||||
}, {
|
||||
Name: "client_with_id",
|
||||
ClientIDs: []string{cliID},
|
||||
}}
|
||||
var (
|
||||
clientWithBothFams = &Persistent{
|
||||
Name: "client1",
|
||||
IPs: []netip.Addr{
|
||||
netip.MustParseAddr(cliIP1),
|
||||
netip.MustParseAddr(cliIPv6),
|
||||
},
|
||||
}
|
||||
|
||||
ci := newIDIndex(clients)
|
||||
clientWithSubnet = &Persistent{
|
||||
Name: "client2",
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
|
||||
}
|
||||
|
||||
clientWithMAC = &Persistent{
|
||||
Name: "client_with_mac",
|
||||
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
|
||||
}
|
||||
|
||||
clientWithID = &Persistent{
|
||||
Name: "client_with_id",
|
||||
ClientIDs: []string{cliID},
|
||||
}
|
||||
|
||||
clientLinkLocal = &Persistent{
|
||||
Name: "client_link_local",
|
||||
Subnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)},
|
||||
}
|
||||
)
|
||||
|
||||
ci := newIDIndex([]*Persistent{
|
||||
clientWithBothFams,
|
||||
clientWithSubnet,
|
||||
clientWithMAC,
|
||||
clientWithID,
|
||||
clientLinkLocal,
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
want *Persistent
|
||||
|
@ -64,19 +86,23 @@ func TestClientIndex(t *testing.T) {
|
|||
}{{
|
||||
name: "ipv4_ipv6",
|
||||
ids: []string{cliIP1, cliIPv6},
|
||||
want: clients[0],
|
||||
want: clientWithBothFams,
|
||||
}, {
|
||||
name: "ipv4_subnet",
|
||||
ids: []string{cliIP2, cliSubnetIP},
|
||||
want: clients[1],
|
||||
want: clientWithSubnet,
|
||||
}, {
|
||||
name: "mac",
|
||||
ids: []string{cliMAC},
|
||||
want: clients[2],
|
||||
want: clientWithMAC,
|
||||
}, {
|
||||
name: "client_id",
|
||||
ids: []string{cliID},
|
||||
want: clients[3],
|
||||
want: clientWithID,
|
||||
}, {
|
||||
name: "client_link_local_subnet",
|
||||
ids: []string{linkLocalIP},
|
||||
want: clientLinkLocal,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
@ -221,3 +247,52 @@ func TestMACToKey(t *testing.T) {
|
|||
_ = macToKey(mac)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIndex_FindByIPWithoutZone(t *testing.T) {
|
||||
var (
|
||||
ip = netip.MustParseAddr("fe80::a098:7654:32ef:ff1")
|
||||
ipWithZone = netip.MustParseAddr("fe80::1ff:fe23:4567:890a%eth2")
|
||||
)
|
||||
|
||||
var (
|
||||
clientNoZone = &Persistent{
|
||||
Name: "client",
|
||||
IPs: []netip.Addr{ip},
|
||||
}
|
||||
|
||||
clientWithZone = &Persistent{
|
||||
Name: "client_with_zone",
|
||||
IPs: []netip.Addr{ipWithZone},
|
||||
}
|
||||
)
|
||||
|
||||
ci := newIDIndex([]*Persistent{
|
||||
clientNoZone,
|
||||
clientWithZone,
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
ip netip.Addr
|
||||
want *Persistent
|
||||
name string
|
||||
}{{
|
||||
name: "without_zone",
|
||||
ip: ip,
|
||||
want: clientNoZone,
|
||||
}, {
|
||||
name: "with_zone",
|
||||
ip: ipWithZone,
|
||||
want: clientWithZone,
|
||||
}, {
|
||||
name: "zero_address",
|
||||
ip: netip.Addr{},
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := ci.FindByIPWithoutZone(tc.ip.WithZone(""))
|
||||
require.Equal(t, tc.want, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,9 +64,7 @@ type Persistent struct {
|
|||
// upstream must be used.
|
||||
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 *filtering.BlockedServices
|
||||
|
@ -95,6 +93,9 @@ type Persistent struct {
|
|||
UseOwnBlockedServices bool
|
||||
IgnoreQueryLog 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.
|
||||
|
|
|
@ -18,7 +18,7 @@ var _ proxy.BeforeRequestHandler = (*Server)(nil)
|
|||
// including logs. It performs access checks and puts the client ID, if there
|
||||
// is one, into the server's cache.
|
||||
//
|
||||
// TODO(e.burkov): Write tests.
|
||||
// TODO(d.kolyshev): Extract to separate package.
|
||||
func (s *Server) HandleBefore(
|
||||
_ *proxy.Proxy,
|
||||
pctx *proxy.DNSContext,
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
package dnsforward
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
blockedHost = "blockedhost.org"
|
||||
testFQDN = "example.org."
|
||||
dnsClientTimeout = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
func TestServer_HandleBefore_tls(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const clientID = "client-1"
|
||||
|
||||
testCases := []struct {
|
||||
clientSrvName string
|
||||
name string
|
||||
host string
|
||||
allowedClients []string
|
||||
disallowedClients []string
|
||||
blockedHosts []string
|
||||
wantRCode int
|
||||
}{{
|
||||
clientSrvName: tlsServerName,
|
||||
name: "allow_all",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantRCode: dns.RcodeSuccess,
|
||||
}, {
|
||||
clientSrvName: "%" + "." + tlsServerName,
|
||||
name: "invalid_client_id",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantRCode: dns.RcodeServerFailure,
|
||||
}, {
|
||||
clientSrvName: clientID + "." + tlsServerName,
|
||||
name: "allowed_client_allowed",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{clientID},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantRCode: dns.RcodeSuccess,
|
||||
}, {
|
||||
clientSrvName: "client-2." + tlsServerName,
|
||||
name: "allowed_client_rejected",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{clientID},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantRCode: dns.RcodeRefused,
|
||||
}, {
|
||||
clientSrvName: tlsServerName,
|
||||
name: "disallowed_client_allowed",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{clientID},
|
||||
blockedHosts: []string{},
|
||||
wantRCode: dns.RcodeSuccess,
|
||||
}, {
|
||||
clientSrvName: clientID + "." + tlsServerName,
|
||||
name: "disallowed_client_rejected",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{clientID},
|
||||
blockedHosts: []string{},
|
||||
wantRCode: dns.RcodeRefused,
|
||||
}, {
|
||||
clientSrvName: tlsServerName,
|
||||
name: "blocked_hosts_allowed",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{blockedHost},
|
||||
wantRCode: dns.RcodeSuccess,
|
||||
}, {
|
||||
clientSrvName: tlsServerName,
|
||||
name: "blocked_hosts_rejected",
|
||||
host: dns.Fqdn(blockedHost),
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{blockedHost},
|
||||
wantRCode: dns.RcodeRefused,
|
||||
}}
|
||||
|
||||
localAns := []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: testFQDN,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 4,
|
||||
},
|
||||
A: net.IP{1, 2, 3, 4},
|
||||
}}
|
||||
localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := (&dns.Msg{}).SetReply(req)
|
||||
resp.Answer = localAns
|
||||
|
||||
require.NoError(t, w.WriteMsg(resp))
|
||||
})
|
||||
localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _ := createTestTLS(t, TLSConfig{
|
||||
TLSListenAddrs: []*net.TCPAddr{{}},
|
||||
ServerName: tlsServerName,
|
||||
})
|
||||
|
||||
s.conf.UpstreamDNS = []string{localUpsAddr}
|
||||
|
||||
s.conf.AllowedClients = tc.allowedClients
|
||||
s.conf.DisallowedClients = tc.disallowedClients
|
||||
s.conf.BlockedHosts = tc.blockedHosts
|
||||
|
||||
err := s.Prepare(&s.conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
startDeferStop(t, s)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: tc.clientSrvName,
|
||||
}
|
||||
|
||||
client := &dns.Client{
|
||||
Net: "tcp-tls",
|
||||
TLSConfig: tlsConfig,
|
||||
Timeout: dnsClientTimeout,
|
||||
}
|
||||
|
||||
req := createTestMessage(tc.host)
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoTLS).String()
|
||||
|
||||
reply, _, err := client.Exchange(req, addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantRCode, reply.Rcode)
|
||||
if tc.wantRCode == dns.RcodeSuccess {
|
||||
assert.Equal(t, localAns, reply.Answer)
|
||||
} else {
|
||||
assert.Empty(t, reply.Answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_HandleBefore_udp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
clientIPv4 = "127.0.0.1"
|
||||
clientIPv6 = "::1"
|
||||
)
|
||||
|
||||
clientIPs := []string{clientIPv4, clientIPv6}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
host string
|
||||
allowedClients []string
|
||||
disallowedClients []string
|
||||
blockedHosts []string
|
||||
wantTimeout bool
|
||||
}{{
|
||||
name: "allow_all",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantTimeout: false,
|
||||
}, {
|
||||
name: "allowed_client_allowed",
|
||||
host: testFQDN,
|
||||
allowedClients: clientIPs,
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantTimeout: false,
|
||||
}, {
|
||||
name: "allowed_client_rejected",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{"1:2:3::4"},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{},
|
||||
wantTimeout: true,
|
||||
}, {
|
||||
name: "disallowed_client_allowed",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{"1:2:3::4"},
|
||||
blockedHosts: []string{},
|
||||
wantTimeout: false,
|
||||
}, {
|
||||
name: "disallowed_client_rejected",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: clientIPs,
|
||||
blockedHosts: []string{},
|
||||
wantTimeout: true,
|
||||
}, {
|
||||
name: "blocked_hosts_allowed",
|
||||
host: testFQDN,
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{blockedHost},
|
||||
wantTimeout: false,
|
||||
}, {
|
||||
name: "blocked_hosts_rejected",
|
||||
host: dns.Fqdn(blockedHost),
|
||||
allowedClients: []string{},
|
||||
disallowedClients: []string{},
|
||||
blockedHosts: []string{blockedHost},
|
||||
wantTimeout: true,
|
||||
}}
|
||||
|
||||
localAns := []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: testFQDN,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3600,
|
||||
Rdlength: 4,
|
||||
},
|
||||
A: net.IP{1, 2, 3, 4},
|
||||
}}
|
||||
localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := (&dns.Msg{}).SetReply(req)
|
||||
resp.Answer = localAns
|
||||
|
||||
require.NoError(t, w.WriteMsg(resp))
|
||||
})
|
||||
localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
Config: Config{
|
||||
AllowedClients: tc.allowedClients,
|
||||
DisallowedClients: tc.disallowedClients,
|
||||
BlockedHosts: tc.blockedHosts,
|
||||
UpstreamDNS: []string{localUpsAddr},
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
})
|
||||
|
||||
startDeferStop(t, s)
|
||||
|
||||
client := &dns.Client{
|
||||
Net: "udp",
|
||||
Timeout: dnsClientTimeout,
|
||||
}
|
||||
|
||||
req := createTestMessage(tc.host)
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
|
||||
|
||||
reply, _, err := client.Exchange(req, addr)
|
||||
if tc.wantTimeout {
|
||||
wantErr := &net.OpError{}
|
||||
require.ErrorAs(t, err, &wantErr)
|
||||
assert.True(t, wantErr.Timeout())
|
||||
|
||||
assert.Nil(t, reply)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, reply.Rcode)
|
||||
assert.Equal(t, localAns, reply.Answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package dnsforward
|
|||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
|
@ -491,19 +490,10 @@ func TestServerRace(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{
|
||||
Enabled: true,
|
||||
Google: true,
|
||||
Yandex: true,
|
||||
CustomResolver: resolver,
|
||||
Enabled: true,
|
||||
Google: true,
|
||||
Yandex: true,
|
||||
}
|
||||
|
||||
filterConf := &filtering.Config{
|
||||
|
@ -540,7 +530,6 @@ func TestSafeSearch(t *testing.T) {
|
|||
client := &dns.Client{}
|
||||
|
||||
yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})
|
||||
googleIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
testCases := []struct {
|
||||
host string
|
||||
|
@ -564,19 +553,19 @@ func TestSafeSearch(t *testing.T) {
|
|||
wantCNAME: "",
|
||||
}, {
|
||||
host: "www.google.com.",
|
||||
want: googleIP,
|
||||
want: netip.Addr{},
|
||||
wantCNAME: "forcesafesearch.google.com.",
|
||||
}, {
|
||||
host: "www.google.com.af.",
|
||||
want: googleIP,
|
||||
want: netip.Addr{},
|
||||
wantCNAME: "forcesafesearch.google.com.",
|
||||
}, {
|
||||
host: "www.google.be.",
|
||||
want: googleIP,
|
||||
want: netip.Addr{},
|
||||
wantCNAME: "forcesafesearch.google.com.",
|
||||
}, {
|
||||
host: "www.google.by.",
|
||||
want: googleIP,
|
||||
want: netip.Addr{},
|
||||
wantCNAME: "forcesafesearch.google.com.",
|
||||
}}
|
||||
|
||||
|
@ -593,12 +582,15 @@ func TestSafeSearch(t *testing.T) {
|
|||
|
||||
cname := testutil.RequireTypeAssert[*dns.CNAME](t, reply.Answer[0])
|
||||
assert.Equal(t, tc.wantCNAME, cname.Target)
|
||||
|
||||
a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[1])
|
||||
assert.NotEmpty(t, a.A)
|
||||
} else {
|
||||
require.Len(t, reply.Answer, 1)
|
||||
}
|
||||
|
||||
a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[len(reply.Answer)-1])
|
||||
assert.Equal(t, net.IP(tc.want.AsSlice()), a.A)
|
||||
a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])
|
||||
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
|
||||
q := req.Question[0]
|
||||
host := strings.TrimSuffix(q.Name, ".")
|
||||
|
||||
resVal, err := s.dnsFilter.CheckHost(host, q.Qtype, dctx.setts)
|
||||
if err != nil {
|
||||
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.
|
||||
res = &resVal
|
||||
switch {
|
||||
case res.IsFiltered:
|
||||
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:
|
||||
case isRewrittenCNAME(res):
|
||||
// Resolve the new canonical name, not the original host name. The
|
||||
// original question is readded in processFilteringAfterResponse.
|
||||
dctx.origQuestion = q
|
||||
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)
|
||||
case res.Reason.In(filtering.RewrittenRule, filtering.RewrittenAutoHosts):
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// use.
|
||||
func (s *Server) checkHostRules(
|
||||
|
|
|
@ -52,7 +52,7 @@ func (s *Server) genDNSFilterMessage(
|
|||
) (resp *dns.Msg) {
|
||||
req := dctx.Req
|
||||
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()
|
||||
if m == filtering.BlockingModeNullIP {
|
||||
return s.replyCompressed(req)
|
||||
|
|
|
@ -380,8 +380,12 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
|
|||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
req := pctx.Req
|
||||
q := req.Question[0]
|
||||
pref := pctx.RequestedPrivateRDNS
|
||||
if pref == (netip.Prefix{}) {
|
||||
// TODO(e.burkov): Consider answering authoritatively for SOA and NS
|
||||
// queries.
|
||||
if pref == (netip.Prefix{}) || q.Qtype != dns.TypePTR {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
|
@ -393,11 +397,10 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
|
|||
|
||||
log.Debug("dnsforward: dhcp client %s is %q", addr, host)
|
||||
|
||||
req := pctx.Req
|
||||
resp := s.replyCompressed(req)
|
||||
ptr := &dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
// TODO(e.burkov): Use [dhcpsvc.Lease.Expiry]. See
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/3932.
|
||||
|
@ -598,7 +601,8 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
|
|||
return resultCodeSuccess
|
||||
case
|
||||
filtering.Rewritten,
|
||||
filtering.RewrittenRule:
|
||||
filtering.RewrittenRule,
|
||||
filtering.FilteredSafeSearch:
|
||||
|
||||
if dctx.origQuestion.Name == "" {
|
||||
// origQuestion is set in case we get only CNAME without IP from
|
||||
|
@ -608,11 +612,10 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
|
|||
|
||||
pctx := dctx.proxyCtx
|
||||
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)
|
||||
answer := append([]dns.RR{rr}, pctx.Res.Answer...)
|
||||
pctx.Res.Answer = answer
|
||||
}
|
||||
|
||||
rr := s.genAnswerCNAME(pctx.Req, res.CanonName)
|
||||
answer := append([]dns.RR{rr}, pctx.Res.Answer...)
|
||||
pctx.Res.Answer = answer
|
||||
|
||||
return resultCodeSuccess
|
||||
default:
|
||||
|
|
|
@ -559,6 +559,8 @@ type Result struct {
|
|||
Reason Reason `json:",omitempty"`
|
||||
|
||||
// IsFiltered is true if the request is filtered.
|
||||
//
|
||||
// TODO(d.kolyshev): Get rid of this flag.
|
||||
IsFiltered bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package filtering
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// SafeSearch interface describes a service for search engines hosts rewrites.
|
||||
type SafeSearch interface {
|
||||
// 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.
|
||||
type SafeSearchConfig struct {
|
||||
// CustomResolver is the resolver used by safe search.
|
||||
CustomResolver Resolver `yaml:"-" json:"-"`
|
||||
|
||||
// Enabled indicates if safe search is enabled entirely.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
|
||||
|
@ -40,13 +35,7 @@ func (d *DNSFilter) checkSafeSearch(
|
|||
qtype uint16,
|
||||
setts *Settings,
|
||||
) (res Result, err error) {
|
||||
if !setts.ProtectionEnabled ||
|
||||
!setts.SafeSearchEnabled ||
|
||||
(qtype != dns.TypeA && qtype != dns.TypeAAAA) {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
if d.safeSearch == nil {
|
||||
if d.safeSearch == nil || !setts.ProtectionEnabled || !setts.SafeSearchEnabled {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,9 @@ package safesearch
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -67,7 +65,6 @@ type Default struct {
|
|||
engine *urlfilter.DNSEngine
|
||||
|
||||
cache cache.Cache
|
||||
resolver filtering.Resolver
|
||||
logPrefix string
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
@ -80,11 +77,6 @@ func NewDefault(
|
|||
cacheSize uint,
|
||||
cacheTTL time.Duration,
|
||||
) (ss *Default, err error) {
|
||||
var resolver filtering.Resolver = net.DefaultResolver
|
||||
if conf.CustomResolver != nil {
|
||||
resolver = conf.CustomResolver
|
||||
}
|
||||
|
||||
ss = &Default{
|
||||
mu: &sync.RWMutex{},
|
||||
|
||||
|
@ -92,7 +84,6 @@ func NewDefault(
|
|||
EnableLRU: true,
|
||||
MaxSize: cacheSize,
|
||||
}),
|
||||
resolver: resolver,
|
||||
// Use %s, because the client safe-search names already contain double
|
||||
// quotes.
|
||||
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))
|
||||
}()
|
||||
|
||||
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
|
||||
return filtering.Result{}, fmt.Errorf("unsupported question type %s", dns.Type(qtype))
|
||||
switch qtype {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeHTTPS:
|
||||
// Go on.
|
||||
default:
|
||||
return filtering.Result{}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// TODO(a.garipov): Consider switch back to resolving CNAME records IPs and
|
||||
// saving results to cache.
|
||||
ss.setCacheResult(host, qtype, res)
|
||||
|
||||
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
|
||||
// [dns.TypeA] or [dns.TypeAAAA]. If err is nil, res is 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.
|
||||
// [dns.TypeA] or [dns.TypeAAAA], or [dns.TypeHTTPS]. If err is nil, res is
|
||||
// never nil, so that the empty result is converted into a NODATA response.
|
||||
func (ss *Default) newResult(
|
||||
rewrite *rules.DNSRewrite,
|
||||
qtype rules.RRType,
|
||||
) (res *filtering.Result, err error) {
|
||||
res = &filtering.Result{
|
||||
Rules: []*filtering.ResultRule{{
|
||||
FilterListID: rulelist.URLFilterIDSafeSearch,
|
||||
}},
|
||||
Reason: filtering.FilteredSafeSearch,
|
||||
IsFiltered: true,
|
||||
}
|
||||
|
@ -247,69 +237,19 @@ func (ss *Default) newResult(
|
|||
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
|
||||
}
|
||||
|
||||
host := 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
|
||||
}
|
||||
res.CanonName = rewrite.NewCNAME
|
||||
|
||||
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
|
||||
// [dns.TypeA] or [dns.TypeAAAA].
|
||||
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package safesearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
|
@ -79,47 +76,6 @@ func TestSafeSearchCacheYandex(t *testing.T) {
|
|||
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"
|
||||
|
||||
var dnsRewriteSink *rules.DNSRewrite
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||
|
@ -31,8 +30,6 @@ const (
|
|||
|
||||
// testConf is the default safe search configuration for tests.
|
||||
var testConf = filtering.SafeSearchConfig{
|
||||
CustomResolver: nil,
|
||||
|
||||
Enabled: true,
|
||||
|
||||
Bing: true,
|
||||
|
@ -52,61 +49,60 @@ func TestDefault_CheckHost_yandex(t *testing.T) {
|
|||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
hosts := []string{
|
||||
"yandex.ru",
|
||||
"yAndeX.ru",
|
||||
"YANdex.COM",
|
||||
"yandex.by",
|
||||
"yandex.kz",
|
||||
"www.yandex.com",
|
||||
} {
|
||||
var res filtering.Result
|
||||
res, err = ss.CheckHost(host, testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, yandexIP, res.Rules[0].IP)
|
||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefault_CheckHost_yandexAAAA(t *testing.T) {
|
||||
conf := testConf
|
||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
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,
|
||||
}}
|
||||
|
||||
res, err := ss.CheckHost("www.yandex.ru", dns.TypeAAAA)
|
||||
require.NoError(t, err)
|
||||
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
|
||||
res, err = ss.CheckHost(host, tc.qt)
|
||||
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
|
||||
// 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)
|
||||
if tc.want == (netip.Addr{}) {
|
||||
assert.Empty(t, res.Rules)
|
||||
} else {
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Empty(t, res.Rules[0].IP)
|
||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
||||
rule := res.Rules[0]
|
||||
assert.Equal(t, tc.want, rule.IP)
|
||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, rule.FilterListID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefault_CheckHost_google(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
|
||||
},
|
||||
}
|
||||
|
||||
wantIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
conf := testConf
|
||||
conf.CustomResolver = resolver
|
||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
ss, err := safesearch.NewDefault(testConf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check host for each domain.
|
||||
|
@ -125,11 +121,9 @@ func TestDefault_CheckHost_google(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, wantIP, res.Rules[0].IP)
|
||||
assert.Equal(t, rulelist.URLFilterIDSafeSearch, res.Rules[0].FilterListID)
|
||||
assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
|
||||
assert.Equal(t, "forcesafesearch.google.com", res.CanonName)
|
||||
assert.Empty(t, res.Rules)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -154,17 +148,7 @@ func (r *testResolver) LookupIP(
|
|||
}
|
||||
|
||||
func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
|
||||
conf := testConf
|
||||
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)
|
||||
ss, err := safesearch.NewDefault(testConf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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)
|
||||
|
||||
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)
|
||||
assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
|
||||
assert.Equal(t, "safe.duckduckgo.com", res.CanonName)
|
||||
assert.Empty(t, res.Rules)
|
||||
}
|
||||
|
||||
func TestDefault_Update(t *testing.T) {
|
||||
|
|
|
@ -249,8 +249,6 @@ func (o *clientObject) toPersistent(
|
|||
}
|
||||
|
||||
if o.SafeSearchConf.Enabled {
|
||||
o.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
||||
|
||||
err = cli.SetSafeSearch(
|
||||
o.SafeSearchConf,
|
||||
filteringConf.SafeSearchCacheSize,
|
||||
|
@ -414,7 +412,11 @@ func (clients *clientsContainer) clientOrArtificial(
|
|||
}()
|
||||
|
||||
cli, ok := clients.find(id)
|
||||
if ok {
|
||||
if !ok {
|
||||
cli = clients.clientIndex.FindByIPWithoutZone(ip)
|
||||
}
|
||||
|
||||
if cli != nil {
|
||||
return &querylog.Client{
|
||||
Name: cli.Name,
|
||||
IgnoreQueryLog: cli.IgnoreQueryLog,
|
||||
|
|
|
@ -203,15 +203,24 @@ type dnsConfig struct {
|
|||
// resolver should be used.
|
||||
PrivateNets []netutil.Prefix `yaml:"private_networks"`
|
||||
|
||||
// UsePrivateRDNS defines if the PTR requests for unknown addresses from
|
||||
// locally-served networks should be resolved via private PTR resolvers.
|
||||
// UsePrivateRDNS enables resolving requests containing a private IP address
|
||||
// using private reverse DNS resolvers. See PrivateRDNSResolvers.
|
||||
//
|
||||
// TODO(e.burkov): Rename in YAML.
|
||||
UsePrivateRDNS bool `yaml:"use_private_ptr_resolvers"`
|
||||
|
||||
// LocalPTRResolvers is the slice of addresses to be used as upstreams
|
||||
// for PTR queries for locally-served networks.
|
||||
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
|
||||
// PrivateRDNSResolvers is the slice of addresses to be used as upstreams
|
||||
// for private requests. It's only used for PTR, SOA, and NS queries,
|
||||
// containing an ARPA subdomain, came from the the client with private
|
||||
// address. The address considered private according to PrivateNets.
|
||||
//
|
||||
// If empty, the OS-provided resolvers are used for private requests.
|
||||
PrivateRDNSResolvers []string `yaml:"local_ptr_upstreams"`
|
||||
|
||||
// UseDNS64 defines if DNS64 should be used for incoming requests.
|
||||
// UseDNS64 defines if DNS64 should be used for incoming requests. Requests
|
||||
// of type PTR for addresses within the configured prefixes will be resolved
|
||||
// via [PrivateRDNSResolvers], so those should be valid and UsePrivateRDNS
|
||||
// be set to true.
|
||||
UseDNS64 bool `yaml:"use_dns64"`
|
||||
|
||||
// DNS64Prefixes is the list of NAT64 prefixes to be used for DNS64.
|
||||
|
@ -658,7 +667,7 @@ func (c *configuration) write() (err error) {
|
|||
dns := &config.DNS
|
||||
dns.Config = c
|
||||
|
||||
dns.LocalPTRResolvers = s.LocalPTRResolvers()
|
||||
dns.PrivateRDNSResolvers = s.LocalPTRResolvers()
|
||||
|
||||
addrProcConf := s.AddrProcConfig()
|
||||
config.Clients.Sources.RDNS = addrProcConf.UseRDNS
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
@ -242,7 +241,7 @@ func newServerConfig(
|
|||
TLSv12Roots: Context.tlsRoots,
|
||||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpReg,
|
||||
LocalPTRResolvers: dnsConf.LocalPTRResolvers,
|
||||
LocalPTRResolvers: dnsConf.PrivateRDNSResolvers,
|
||||
UseDNS64: dnsConf.UseDNS64,
|
||||
DNS64Prefixes: dnsConf.DNS64Prefixes,
|
||||
UsePrivateRDNS: dnsConf.UsePrivateRDNS,
|
||||
|
@ -528,36 +527,6 @@ func closeDNSServer() {
|
|||
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
|
||||
// statistics and query log.
|
||||
func checkStatsAndQuerylogDirs(
|
||||
|
|
|
@ -439,7 +439,6 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
|
|||
conf.ParentalBlockHost = host
|
||||
}
|
||||
|
||||
conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
||||
conf.SafeSearch, err = safesearch.NewDefault(
|
||||
conf.SafeSearchConf,
|
||||
"default",
|
||||
|
|
|
@ -649,11 +649,6 @@ status() {
|
|||
|
||||
// freeBSDScript is the source of the daemon script for FreeBSD. Keep as close
|
||||
// as possible to the https://github.com/kardianos/service/blob/18c957a3dc1120a2efe77beb401d476bade9e577/service_freebsd.go#L204.
|
||||
//
|
||||
// TODO(a.garipov): Don't use .WorkingDirectory here. There are currently no
|
||||
// guarantees that it will actually be the required directory.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2614.
|
||||
const freeBSDScript = `#!/bin/sh
|
||||
# PROVIDE: {{.Name}}
|
||||
# REQUIRE: networking
|
||||
|
@ -667,7 +662,9 @@ name="{{.Name}}"
|
|||
pidfile_child="/var/run/${name}.pid"
|
||||
pidfile="/var/run/${name}_daemon.pid"
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-P ${pidfile} -p ${pidfile_child} -T ${name} -r {{.WorkingDirectory}}/{{.Name}}"
|
||||
daemon_args="-P ${pidfile} -p ${pidfile_child} -r -t ${name}"
|
||||
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||
|
||||
run_rc_command "$1"
|
||||
`
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ type logEntry struct {
|
|||
Answer []byte `json:",omitempty"`
|
||||
OrigAnswer []byte `json:",omitempty"`
|
||||
|
||||
// TODO(s.chzhen): Use netip.Addr.
|
||||
IP net.IP `json:"IP"`
|
||||
|
||||
Result filtering.Result
|
||||
|
|
Loading…
Reference in New Issue