all: sync more

This commit is contained in:
Ainar Garipov 2022-06-02 17:55:48 +03:00
parent dcb043df5f
commit 4f5131f423
31 changed files with 1073 additions and 900 deletions

View File

@ -17,7 +17,6 @@ and this project adheres to
### Security ### Security
- Enforced password strength policy ([#3503]).
- Weaker cipher suites that use the CBC (cipher block chaining) mode of - Weaker cipher suites that use the CBC (cipher block chaining) mode of
operation have been disabled ([#2993]). operation have been disabled ([#2993]).
@ -25,15 +24,65 @@ and this project adheres to
- Support for Discovery of Designated Resolvers (DDR) according to the [RFC - Support for Discovery of Designated Resolvers (DDR) according to the [RFC
draft][ddr-draft-06] ([#4463]). draft][ddr-draft-06] ([#4463]).
- `windows/arm64` support ([#3057]).
### Deprecated
- Go 1.17 support. v0.109.0 will require at least Go 1.18 to build.
[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993
[#3057]: https://github.com/AdguardTeam/AdGuardHome/issues/3057
[ddr-draft-06]: https://www.ietf.org/archive/id/draft-ietf-add-ddr-06.html
<!--
## [v0.107.8] - 2022-07-12 (APPROX.)
-->
## [v0.107.7] - 2022-06-06 (APPROX.)
See also the [v0.107.7 GitHub milestone][ms-v0.107.7].
### Security
- Go version was updated to prevent the possibility of exploiting the
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
[CVE-2022-29804] vulnerabilities.
- Enforced password strength policy ([#3503]).
### Added
- Support for the final DNS-over-QUIC standard, [RFC 9250][rfc-9250] ([#4592]).
- Support upstreams for subdomains of a domain only ([#4503]).
- The ability to control each source of runtime clients separately via - The ability to control each source of runtime clients separately via
`clients.runtime_sources` configuration object ([#3020]). `clients.runtime_sources` configuration object ([#3020]).
- The ability to customize the set of networks that are considered private - The ability to customize the set of networks that are considered private
through the new `dns.private_networks` property in the configuration file through the new `dns.private_networks` property in the configuration file
([#3142]). ([#3142]).
- `windows/arm64` support ([#3057]). - EDNS Client-Subnet information in the request details section of a query log
record ([#3978]).
- Support for hostnames for plain UDP upstream servers using the `udp://` scheme
([#4166]).
- Logs are now collected by default on FreeBSD and OpenBSD when AdGuard Home is
installed as a service ([#4213]).
### Changed ### Changed
- On OpenBSD, the daemon script now uses the recommended `/bin/ksh` shell
instead of the `/bin/sh` one ([#4533]). To apply this change, backup your
data and run `AdGuardHome -s uninstall && AdGuardHome -s install`.
- The default DNS-over-QUIC port number is now `853` instead of `754` in
accordance with [RFC 9250][rfc-9250] ([#4276]).
- Reverse DNS now has a greater priority as the source of runtime clients'
information than ARP neighborhood.
- Improved detection of runtime clients through more resilient ARP processing
([#3597]).
- The TTL of responses served from the optimistic cache is now lowered to 10
seconds.
- Domain-specific private reverse DNS upstream servers are now validated to - Domain-specific private reverse DNS upstream servers are now validated to
allow only `*.in-addr.arpa` and `*.ip6.arpa` domains pointing to allow only `*.in-addr.arpa` and `*.ip6.arpa` domains pointing to
locally-served networks ([#3381]). **Note:** If you already have invalid locally-served networks ([#3381]). **Note:** If you already have invalid
@ -41,8 +90,15 @@ and this project adheres to
essentially had no effect. essentially had no effect.
- Response filtering is now performed using the record types of the answer - Response filtering is now performed using the record types of the answer
section of messages as opposed to the type of the question ([#4238]). section of messages as opposed to the type of the question ([#4238]).
- Instead of adding the build time information, the build scripts now use the
standardized environment variable [`SOURCE_DATE_EPOCH`][repr] to add the date
of the commit from which the binary was built ([#4221]). This should simplify
reproducible builds for package maintainers and those who compile their own
AdGuard Home.
- The property `local_domain_name` is now in the `dhcp` object in the - The property `local_domain_name` is now in the `dhcp` object in the
configuration file to avoid confusion ([#3367]). configuration file to avoid confusion ([#3367]).
- The `dns.bogus_nxdomain` property in the configuration file now supports CIDR
notation alongside IP addresses ([#1730]).
#### Configuration Changes #### Configuration Changes
@ -96,71 +152,9 @@ In this release, the schema version has changed from 12 to 14.
### Deprecated ### Deprecated
- The `--no-etc-hosts` option. Its' functionality is now controlled by - The `--no-etc-hosts` option. Its functionality is now controlled by
`clients.runtime_sources.hosts` configuration property. v0.109.0 will remove `clients.runtime_sources.hosts` configuration property. v0.109.0 will remove
the flag completely. the flag completely.
- Go 1.17 support. v0.109.0 will require at least Go 1.18 to build.
[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993
[#3020]: https://github.com/AdguardTeam/AdGuardHome/issues/3020
[#3057]: https://github.com/AdguardTeam/AdGuardHome/issues/3057
[#3142]: https://github.com/AdguardTeam/AdGuardHome/issues/3142
[#3367]: https://github.com/AdguardTeam/AdGuardHome/issues/3367
[#3381]: https://github.com/AdguardTeam/AdGuardHome/issues/3381
[#3503]: https://github.com/AdguardTeam/AdGuardHome/issues/3503
[#4238]: https://github.com/AdguardTeam/AdGuardHome/issues/4238
[ddr-draft-06]: https://www.ietf.org/archive/id/draft-ietf-add-ddr-06.html
<!--
## [v0.107.8] - 2022-07-12 (APPROX.)
-->
## [v0.107.7] - 2022-06-03 (APPROX.)
See also the [v0.107.7 GitHub milestone][ms-v0.107.7].
### Security
- Go version was updated to prevent the possibility of exploiting the
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
[CVE-2022-29804] vulnerabilities.
### Added
- Support for the final DNS-over-QUIC standard, [RFC 9250][rfc-9250] ([#4592]).
- Support upstreams for subdomains of a domain only ([#4503]).
- EDNS Client-Subnet information in the request details section of a query log
record ([#3978]).
- Support for hostnames for plain UDP upstream servers using the `udp://` scheme
([#4166]).
- Logs are now collected by default on FreeBSD and OpenBSD when AdGuard Home is
installed as a service ([#4213]).
### Changed
- On OpenBSD, the daemon script now uses the recommended `/bin/ksh` shell
instead of the `/bin/sh` one ([#4533]). To apply this change, backup your
data and run `AdGuardHome -s uninstall && AdGuardHome -s install`.
- The default DNS-over-QUIC port number is now `853` instead of `754` in
accordance with [RFC 9250][rfc-9250] ([#4276]).
- Reverse DNS now has a greater priority as the source of runtime clients'
information than ARP neighborhood.
- Improved detection of runtime clients through more resilient ARP processing
([#3597]).
- The TTL of responses served from the optimistic cache is now lowered to 10
seconds.
- Instead of adding the build time information, the build scripts now use the
standardized environment variable [`SOURCE_DATE_EPOCH`][repr] to add the date
of the commit from which the binary was built ([#4221]). This should simplify
reproducible builds for package maintainers and those who compile their own
AdGuard Home.
- The `dns.bogus_nxdomain` property in the configuration file now supports CIDR
notation alongside IP addresses ([#1730]).
### Fixed ### Fixed
@ -172,12 +166,18 @@ See also the [v0.107.7 GitHub milestone][ms-v0.107.7].
- ARP tables refreshing process causing excessive PTR requests ([#3157]). - ARP tables refreshing process causing excessive PTR requests ([#3157]).
[#1730]: https://github.com/AdguardTeam/AdGuardHome/issues/1730 [#1730]: https://github.com/AdguardTeam/AdGuardHome/issues/1730
[#3020]: https://github.com/AdguardTeam/AdGuardHome/issues/3020
[#3142]: https://github.com/AdguardTeam/AdGuardHome/issues/3142
[#3157]: https://github.com/AdguardTeam/AdGuardHome/issues/3157 [#3157]: https://github.com/AdguardTeam/AdGuardHome/issues/3157
[#3367]: https://github.com/AdguardTeam/AdGuardHome/issues/3367
[#3381]: https://github.com/AdguardTeam/AdGuardHome/issues/3381
[#3503]: https://github.com/AdguardTeam/AdGuardHome/issues/3503
[#3597]: https://github.com/AdguardTeam/AdGuardHome/issues/3597 [#3597]: https://github.com/AdguardTeam/AdGuardHome/issues/3597
[#3978]: https://github.com/AdguardTeam/AdGuardHome/issues/3978 [#3978]: https://github.com/AdguardTeam/AdGuardHome/issues/3978
[#4166]: https://github.com/AdguardTeam/AdGuardHome/issues/4166 [#4166]: https://github.com/AdguardTeam/AdGuardHome/issues/4166
[#4213]: https://github.com/AdguardTeam/AdGuardHome/issues/4213 [#4213]: https://github.com/AdguardTeam/AdGuardHome/issues/4213
[#4221]: https://github.com/AdguardTeam/AdGuardHome/issues/4221 [#4221]: https://github.com/AdguardTeam/AdGuardHome/issues/4221
[#4238]: https://github.com/AdguardTeam/AdGuardHome/issues/4238
[#4273]: https://github.com/AdguardTeam/AdGuardHome/issues/4273 [#4273]: https://github.com/AdguardTeam/AdGuardHome/issues/4273
[#4276]: https://github.com/AdguardTeam/AdGuardHome/issues/4276 [#4276]: https://github.com/AdguardTeam/AdGuardHome/issues/4276
[#4480]: https://github.com/AdguardTeam/AdGuardHome/issues/4480 [#4480]: https://github.com/AdguardTeam/AdGuardHome/issues/4480

View File

@ -1,156 +0,0 @@
package aghnet
import (
"net"
)
// SubnetDetector describes IP address properties.
type SubnetDetector struct {
// spNets is the slice of special-purpose address registries as defined
// by RFC-6890 (https://tools.ietf.org/html/rfc6890).
spNets []*net.IPNet
// locServedNets is the slice of locally-served networks as defined by
// RFC-6303 (https://tools.ietf.org/html/rfc6303).
locServedNets []*net.IPNet
}
// NewSubnetDetector returns a new IP detector.
func NewSubnetDetector() (snd *SubnetDetector, err error) {
spNets := []string{
// "This" network.
"0.0.0.0/8",
// Private-Use Networks.
"10.0.0.0/8",
// Shared Address Space.
"100.64.0.0/10",
// Loopback.
"127.0.0.0/8",
// Link Local.
"169.254.0.0/16",
// Private-Use Networks.
"172.16.0.0/12",
// IETF Protocol Assignments.
"192.0.0.0/24",
// DS-Lite.
"192.0.0.0/29",
// TEST-NET-1
"192.0.2.0/24",
// 6to4 Relay Anycast.
"192.88.99.0/24",
// Private-Use Networks.
"192.168.0.0/16",
// Network Interconnect Device Benchmark Testing.
"198.18.0.0/15",
// TEST-NET-2.
"198.51.100.0/24",
// TEST-NET-3.
"203.0.113.0/24",
// Reserved for Future Use.
"240.0.0.0/4",
// Limited Broadcast.
"255.255.255.255/32",
// Loopback.
"::1/128",
// Unspecified.
"::/128",
// IPv4-IPv6 Translation Address.
"64:ff9b::/96",
// IPv4-Mapped Address. Since this network is used for mapping
// IPv4 addresses, we don't include it.
// "::ffff:0:0/96",
// Discard-Only Prefix.
"100::/64",
// IETF Protocol Assignments.
"2001::/23",
// TEREDO.
"2001::/32",
// Benchmarking.
"2001:2::/48",
// Documentation.
"2001:db8::/32",
// ORCHID.
"2001:10::/28",
// 6to4.
"2002::/16",
// Unique-Local.
"fc00::/7",
// Linked-Scoped Unicast.
"fe80::/10",
}
// TODO(e.burkov): It's a subslice of the slice above. Should be done
// smarter.
locServedNets := []string{
// IPv4.
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"169.254.0.0/16",
"192.0.2.0/24",
"198.51.100.0/24",
"203.0.113.0/24",
"255.255.255.255/32",
// IPv6.
"::/128",
"::1/128",
"fe80::/10",
"2001:db8::/32",
"fd00::/8",
}
snd = &SubnetDetector{
spNets: make([]*net.IPNet, len(spNets)),
locServedNets: make([]*net.IPNet, len(locServedNets)),
}
for i, ipnetStr := range spNets {
var ipnet *net.IPNet
_, ipnet, err = net.ParseCIDR(ipnetStr)
if err != nil {
return nil, err
}
snd.spNets[i] = ipnet
}
for i, ipnetStr := range locServedNets {
var ipnet *net.IPNet
_, ipnet, err = net.ParseCIDR(ipnetStr)
if err != nil {
return nil, err
}
snd.locServedNets[i] = ipnet
}
return snd, nil
}
// anyNetContains ranges through the given ipnets slice searching for the one
// which contains the ip. For internal use only.
//
// TODO(e.burkov): Think about memoization.
func anyNetContains(ipnets *[]*net.IPNet, ip net.IP) (is bool) {
for _, ipnet := range *ipnets {
if ipnet.Contains(ip) {
return true
}
}
return false
}
// IsSpecialNetwork returns true if IP address is contained by any of
// special-purpose IP address registries. It's safe for concurrent use.
func (snd *SubnetDetector) IsSpecialNetwork(ip net.IP) (is bool) {
return anyNetContains(&snd.spNets, ip)
}
// IsLocallyServedNetwork returns true if IP address is contained by any of
// locally-served IP address registries. It's safe for concurrent use.
func (snd *SubnetDetector) IsLocallyServedNetwork(ip net.IP) (is bool) {
return anyNetContains(&snd.locServedNets, ip)
}

View File

@ -1,252 +0,0 @@
package aghnet
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSubnetDetector_DetectSpecialNetwork(t *testing.T) {
snd, err := NewSubnetDetector()
require.NoError(t, err)
testCases := []struct {
name string
ip net.IP
want bool
}{{
name: "not_specific",
ip: net.ParseIP("8.8.8.8"),
want: false,
}, {
name: "this_host_on_this_network",
ip: net.ParseIP("0.0.0.0"),
want: true,
}, {
name: "private-Use",
ip: net.ParseIP("10.0.0.0"),
want: true,
}, {
name: "shared_address_space",
ip: net.ParseIP("100.64.0.0"),
want: true,
}, {
name: "loopback",
ip: net.ParseIP("127.0.0.0"),
want: true,
}, {
name: "link_local",
ip: net.ParseIP("169.254.0.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("172.16.0.0"),
want: true,
}, {
name: "ietf_protocol_assignments",
ip: net.ParseIP("192.0.0.0"),
want: true,
}, {
name: "ds-lite",
ip: net.ParseIP("192.0.0.0"),
want: true,
}, {
name: "documentation_(test-net-1)",
ip: net.ParseIP("192.0.2.0"),
want: true,
}, {
name: "6to4_relay_anycast",
ip: net.ParseIP("192.88.99.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("192.168.0.0"),
want: true,
}, {
name: "benchmarking",
ip: net.ParseIP("198.18.0.0"),
want: true,
}, {
name: "documentation_(test-net-2)",
ip: net.ParseIP("198.51.100.0"),
want: true,
}, {
name: "documentation_(test-net-3)",
ip: net.ParseIP("203.0.113.0"),
want: true,
}, {
name: "reserved",
ip: net.ParseIP("240.0.0.0"),
want: true,
}, {
name: "limited_broadcast",
ip: net.ParseIP("255.255.255.255"),
want: true,
}, {
name: "loopback_address",
ip: net.ParseIP("::1"),
want: true,
}, {
name: "unspecified_address",
ip: net.ParseIP("::"),
want: true,
}, {
name: "ipv4-ipv6_translation",
ip: net.ParseIP("64:ff9b::"),
want: true,
}, {
name: "discard-only_address_block",
ip: net.ParseIP("100::"),
want: true,
}, {
name: "ietf_protocol_assignments",
ip: net.ParseIP("2001::"),
want: true,
}, {
name: "teredo",
ip: net.ParseIP("2001::"),
want: true,
}, {
name: "benchmarking",
ip: net.ParseIP("2001:2::"),
want: true,
}, {
name: "documentation",
ip: net.ParseIP("2001:db8::"),
want: true,
}, {
name: "orchid",
ip: net.ParseIP("2001:10::"),
want: true,
}, {
name: "6to4",
ip: net.ParseIP("2002::"),
want: true,
}, {
name: "unique-local",
ip: net.ParseIP("fc00::"),
want: true,
}, {
name: "linked-scoped_unicast",
ip: net.ParseIP("fe80::"),
want: true,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, snd.IsSpecialNetwork(tc.ip))
})
}
}
func TestSubnetDetector_DetectLocallyServedNetwork(t *testing.T) {
snd, err := NewSubnetDetector()
require.NoError(t, err)
testCases := []struct {
name string
ip net.IP
want bool
}{{
name: "not_specific",
ip: net.ParseIP("8.8.8.8"),
want: false,
}, {
name: "private-Use",
ip: net.ParseIP("10.0.0.0"),
want: true,
}, {
name: "loopback",
ip: net.ParseIP("127.0.0.0"),
want: true,
}, {
name: "link_local",
ip: net.ParseIP("169.254.0.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("172.16.0.0"),
want: true,
}, {
name: "documentation_(test-net-1)",
ip: net.ParseIP("192.0.2.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("192.168.0.0"),
want: true,
}, {
name: "documentation_(test-net-2)",
ip: net.ParseIP("198.51.100.0"),
want: true,
}, {
name: "documentation_(test-net-3)",
ip: net.ParseIP("203.0.113.0"),
want: true,
}, {
name: "limited_broadcast",
ip: net.ParseIP("255.255.255.255"),
want: true,
}, {
name: "loopback_address",
ip: net.ParseIP("::1"),
want: true,
}, {
name: "unspecified_address",
ip: net.ParseIP("::"),
want: true,
}, {
name: "documentation",
ip: net.ParseIP("2001:db8::"),
want: true,
}, {
name: "linked-scoped_unicast",
ip: net.ParseIP("fe80::"),
want: true,
}, {
name: "locally_assigned",
ip: net.ParseIP("fd00::1"),
want: true,
}, {
name: "not_locally_assigned",
ip: net.ParseIP("fc00::1"),
want: false,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, snd.IsLocallyServedNetwork(tc.ip))
})
}
}
func TestSubnetDetector_Detect_parallel(t *testing.T) {
t.Parallel()
snd, err := NewSubnetDetector()
require.NoError(t, err)
testFunc := func() {
for _, ip := range []net.IP{
net.IPv4allrouter,
net.IPv4allsys,
net.IPv4bcast,
net.IPv4zero,
net.IPv6interfacelocalallnodes,
net.IPv6linklocalallnodes,
net.IPv6linklocalallrouters,
net.IPv6loopback,
net.IPv6unspecified,
} {
_ = snd.IsSpecialNetwork(ip)
_ = snd.IsLocallyServedNetwork(ip)
}
}
const goroutinesNum = 50
for i := 0; i < goroutinesNum; i++ {
go testFunc()
}
}

View File

@ -11,10 +11,10 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// TestUpstream is a mock of real upstream. // Upstream is a mock implementation of upstream.Upstream.
type TestUpstream struct { type Upstream struct {
// CName is a map of hostname to canonical name. // CName is a map of hostname to canonical name.
CName map[string]string CName map[string][]string
// IPv4 is a map of hostname to IPv4. // IPv4 is a map of hostname to IPv4.
IPv4 map[string][]net.IP IPv4 map[string][]net.IP
// IPv6 is a map of hostname to IPv6. // IPv6 is a map of hostname to IPv6.
@ -25,78 +25,45 @@ type TestUpstream struct {
Addr string Addr string
} }
// Exchange implements upstream.Upstream interface for *TestUpstream. // Exchange implements the upstream.Upstream interface for *Upstream.
// //
// TODO(a.garipov): Split further into handlers. // TODO(a.garipov): Split further into handlers.
func (u *TestUpstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) { func (u *Upstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) {
resp = &dns.Msg{} resp = new(dns.Msg).SetReply(m)
resp.SetReply(m)
if len(m.Question) == 0 { if len(m.Question) == 0 {
return nil, fmt.Errorf("question should not be empty") return nil, fmt.Errorf("question should not be empty")
} }
name := m.Question[0].Name q := m.Question[0]
name := q.Name
if cname, ok := u.CName[name]; ok { for _, cname := range u.CName[name] {
ans := &dns.CNAME{ resp.Answer = append(resp.Answer, &dns.CNAME{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME},
Name: name,
Rrtype: dns.TypeCNAME,
},
Target: cname, Target: cname,
} })
resp.Answer = append(resp.Answer, ans)
} }
rrType := m.Question[0].Qtype qtype := q.Qtype
hdr := dns.RR_Header{ hdr := dns.RR_Header{
Name: name, Name: name,
Rrtype: rrType, Rrtype: qtype,
} }
var names []string switch qtype {
var ips []net.IP
switch m.Question[0].Qtype {
case dns.TypeA: case dns.TypeA:
ips = u.IPv4[name] for _, ip := range u.IPv4[name] {
resp.Answer = append(resp.Answer, &dns.A{Hdr: hdr, A: ip})
}
case dns.TypeAAAA: case dns.TypeAAAA:
ips = u.IPv6[name] for _, ip := range u.IPv6[name] {
resp.Answer = append(resp.Answer, &dns.AAAA{Hdr: hdr, AAAA: ip})
}
case dns.TypePTR: case dns.TypePTR:
names = u.Reverse[name] for _, name := range u.Reverse[name] {
} resp.Answer = append(resp.Answer, &dns.PTR{Hdr: hdr, Ptr: name})
for _, ip := range ips {
var ans dns.RR
if rrType == dns.TypeA {
ans = &dns.A{
Hdr: hdr,
A: ip,
}
resp.Answer = append(resp.Answer, ans)
continue
} }
ans = &dns.AAAA{
Hdr: hdr,
AAAA: ip,
}
resp.Answer = append(resp.Answer, ans)
} }
for _, n := range names {
ans := &dns.PTR{
Hdr: hdr,
Ptr: n,
}
resp.Answer = append(resp.Answer, ans)
}
if len(resp.Answer) == 0 { if len(resp.Answer) == 0 {
resp.SetRcode(m, dns.RcodeNameError) resp.SetRcode(m, dns.RcodeNameError)
} }
@ -104,8 +71,8 @@ func (u *TestUpstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) {
return resp, nil return resp, nil
} }
// Address implements upstream.Upstream interface for *TestUpstream. // Address implements upstream.Upstream interface for *Upstream.
func (u *TestUpstream) Address() string { func (u *Upstream) Address() string {
return u.Addr return u.Addr
} }

View File

@ -119,23 +119,28 @@ func (l *Lease) UnmarshalJSON(data []byte) (err error) {
return nil return nil
} }
// ServerConfig - DHCP server configuration // ServerConfig is the configuration for the DHCP server. The order of YAML
// field ordering is important -- yaml fields will mirror ordering from here // fields is important, since the YAML configuration file follows it.
type ServerConfig struct { type ServerConfig struct {
Enabled bool `yaml:"enabled"`
InterfaceName string `yaml:"interface_name"`
Conf4 V4ServerConf `yaml:"dhcpv4"`
Conf6 V6ServerConf `yaml:"dhcpv6"`
WorkDir string `yaml:"-"`
DBFilePath string `yaml:"-"` // path to DB file
// Called when the configuration is changed by HTTP request // Called when the configuration is changed by HTTP request
ConfigModified func() `yaml:"-"` ConfigModified func() `yaml:"-"`
// Register an HTTP handler // Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"` HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
Enabled bool `yaml:"enabled"`
InterfaceName string `yaml:"interface_name"`
// LocalDomainName is the domain name used for DHCP hosts. For example,
// a DHCP client with the hostname "myhost" can be addressed as "myhost.lan"
// when LocalDomainName is "lan".
LocalDomainName string `yaml:"local_domain_name"`
Conf4 V4ServerConf `yaml:"dhcpv4"`
Conf6 V6ServerConf `yaml:"dhcpv6"`
WorkDir string `yaml:"-"`
DBFilePath string `yaml:"-"`
} }
// OnLeaseChangedT is a callback for lease changes. // OnLeaseChangedT is a callback for lease changes.
@ -156,7 +161,9 @@ type Server struct {
srv4 DHCPServer srv4 DHCPServer
srv6 DHCPServer srv6 DHCPServer
conf ServerConfig // TODO(a.garipov): Either create a separate type for the internal config or
// just put the config values into Server.
conf *ServerConfig
// Called when the leases DB is modified // Called when the leases DB is modified
onLeaseChanged []OnLeaseChangedT onLeaseChanged []OnLeaseChangedT
@ -181,14 +188,21 @@ type ServerInterface interface {
} }
// Create - create object // Create - create object
func Create(conf ServerConfig) (s *Server, err error) { func Create(conf *ServerConfig) (s *Server, err error) {
s = &Server{} s = &Server{
conf: &ServerConfig{
ConfigModified: conf.ConfigModified,
s.conf.Enabled = conf.Enabled HTTPRegister: conf.HTTPRegister,
s.conf.InterfaceName = conf.InterfaceName
s.conf.HTTPRegister = conf.HTTPRegister Enabled: conf.Enabled,
s.conf.ConfigModified = conf.ConfigModified InterfaceName: conf.InterfaceName,
s.conf.DBFilePath = filepath.Join(conf.WorkDir, dbFilename)
LocalDomainName: conf.LocalDomainName,
DBFilePath: filepath.Join(conf.WorkDir, dbFilename),
},
}
if !webHandlersRegistered && s.conf.HTTPRegister != nil { if !webHandlersRegistered && s.conf.HTTPRegister != nil {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -305,6 +319,7 @@ func (s *Server) notify(flags int) {
func (s *Server) WriteDiskConfig(c *ServerConfig) { func (s *Server) WriteDiskConfig(c *ServerConfig) {
c.Enabled = s.conf.Enabled c.Enabled = s.conf.Enabled
c.InterfaceName = s.conf.InterfaceName c.InterfaceName = s.conf.InterfaceName
c.LocalDomainName = s.conf.LocalDomainName
s.srv4.WriteDiskConfig4(&c.Conf4) s.srv4.WriteDiskConfig4(&c.Conf4)
s.srv6.WriteDiskConfig6(&c.Conf6) s.srv6.WriteDiskConfig6(&c.Conf6)
} }

View File

@ -27,7 +27,7 @@ func testNotify(flags uint32) {
func TestDB(t *testing.T) { func TestDB(t *testing.T) {
var err error var err error
s := Server{ s := Server{
conf: ServerConfig{ conf: &ServerConfig{
DBFilePath: dbFilename, DBFilePath: dbFilename,
}, },
} }
@ -140,27 +140,27 @@ func TestNormalizeLeases(t *testing.T) {
func TestV4Server_badRange(t *testing.T) { func TestV4Server_badRange(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
wantErrMsg string
gatewayIP net.IP gatewayIP net.IP
subnetMask net.IP subnetMask net.IP
wantErrMsg string
}{{ }{{
name: "gateway_in_range", name: "gateway_in_range",
gatewayIP: net.IP{192, 168, 10, 120},
subnetMask: net.IP{255, 255, 255, 0},
wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " + wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
"192.168.10.20-192.168.10.200", "192.168.10.20-192.168.10.200",
gatewayIP: net.IP{192, 168, 10, 120},
subnetMask: net.IP{255, 255, 255, 0},
}, { }, {
name: "outside_range_start", name: "outside_range_start",
gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 240},
wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " + wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
"192.168.10.1/28", "192.168.10.1/28",
}, {
name: "outside_range_end",
gatewayIP: net.IP{192, 168, 10, 1}, gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 224}, subnetMask: net.IP{255, 255, 255, 240},
}, {
name: "outside_range_end",
wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " + wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
"192.168.10.1/27", "192.168.10.1/27",
gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 224},
}} }}
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -575,12 +575,15 @@ func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
log.Error("dhcp: removing db: %s", err) log.Error("dhcp: removing db: %s", err)
} }
oldconf := s.conf s.conf = &ServerConfig{
s.conf = ServerConfig{ ConfigModified: s.conf.ConfigModified,
WorkDir: oldconf.WorkDir,
HTTPRegister: oldconf.HTTPRegister, HTTPRegister: s.conf.HTTPRegister,
ConfigModified: oldconf.ConfigModified,
DBFilePath: oldconf.DBFilePath, LocalDomainName: s.conf.LocalDomainName,
WorkDir: s.conf.WorkDir,
DBFilePath: s.conf.DBFilePath,
} }
v4conf := V4ServerConf{ v4conf := V4ServerConf{

View File

@ -132,8 +132,9 @@ type FilteringConfig struct {
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS // TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
type TLSConfig struct { type TLSConfig struct {
TLSListenAddrs []*net.TCPAddr `yaml:"-" json:"-"` TLSListenAddrs []*net.TCPAddr `yaml:"-" json:"-"`
QUICListenAddrs []*net.UDPAddr `yaml:"-" json:"-"` QUICListenAddrs []*net.UDPAddr `yaml:"-" json:"-"`
HTTPSListenAddrs []*net.TCPAddr `yaml:"-" json:"-"`
// Reject connection if the client uses server name (in SNI) that doesn't match the certificate // Reject connection if the client uses server name (in SNI) that doesn't match the certificate
StrictSNICheck bool `yaml:"strict_sni_check" json:"-"` StrictSNICheck bool `yaml:"strict_sni_check" json:"-"`

View File

@ -214,9 +214,8 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
ipToHost = netutil.NewIPMap(len(ll)) ipToHost = netutil.NewIPMap(len(ll))
for _, l := range ll { for _, l := range ll {
// TODO(a.garipov): Remove this after we're finished // TODO(a.garipov): Remove this after we're finished with the client
// with the client hostname validations in the DHCP // hostname validations in the DHCP server code.
// server code.
err = netutil.ValidateDomainName(l.Hostname) err = netutil.ValidateDomainName(l.Hostname)
if err != nil { if err != nil {
log.Debug( log.Debug(
@ -252,7 +251,7 @@ func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) {
return rc return rc
} }
dctx.isLocalClient = s.subnetDetector.IsLocallyServedNetwork(ip) dctx.isLocalClient = s.privateNets.Contains(ip)
return rc return rc
} }
@ -300,6 +299,8 @@ func (s *Server) processInternalHosts(dctx *dnsContext) (rc resultCode) {
} }
reqHost := strings.ToLower(q.Name) reqHost := strings.ToLower(q.Name)
// TODO(a.garipov): Move everything related to DHCP local domain to the DHCP
// server.
host := strings.TrimSuffix(reqHost, s.localDomainSuffix) host := strings.TrimSuffix(reqHost, s.localDomainSuffix)
if host == reqHost { if host == reqHost {
return resultCodeSuccess return resultCodeSuccess
@ -372,7 +373,7 @@ func (s *Server) processRestrictLocal(ctx *dnsContext) (rc resultCode) {
// Restrict an access to local addresses for external clients. We also // Restrict an access to local addresses for external clients. We also
// assume that all the DHCP leases we give are locally-served or at least // assume that all the DHCP leases we give are locally-served or at least
// don't need to be inaccessible externally. // don't need to be inaccessible externally.
if !s.subnetDetector.IsLocallyServedNetwork(ip) { if !s.privateNets.Contains(ip) {
log.Debug("dns: addr %s is not from locally-served network", ip) log.Debug("dns: addr %s is not from locally-served network", ip)
return resultCodeSuccess return resultCodeSuccess
@ -479,7 +480,7 @@ func (s *Server) processLocalPTR(ctx *dnsContext) (rc resultCode) {
s.serverLock.RLock() s.serverLock.RLock()
defer s.serverLock.RUnlock() defer s.serverLock.RUnlock()
if !s.subnetDetector.IsLocallyServedNetwork(ip) { if !s.privateNets.Contains(ip) {
return resultCodeSuccess return resultCodeSuccess
} }
@ -611,9 +612,9 @@ func (s *Server) processFilteringAfterResponse(ctx *dnsContext) (rc resultCode)
d.Res.Answer = answer d.Res.Answer = answer
} }
default: default:
// Check the response only if the it's from an upstream. Don't check // Check the response only if it's from an upstream. Don't check the
// the response if the protection is disabled since dnsrewrite rules // response if the protection is disabled since dnsrewrite rules aren't
// aren't applied to it anyway. // applied to it anyway.
if !ctx.protectionEnabled || !ctx.responseFromUpstream || s.dnsFilter == nil { if !ctx.protectionEnabled || !ctx.responseFromUpstream || s.dnsFilter == nil {
break break
} }

View File

@ -4,35 +4,41 @@ import (
"net" "net"
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"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/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestServer_ProcessDetermineLocal(t *testing.T) { func TestServer_ProcessDetermineLocal(t *testing.T) {
snd, err := aghnet.NewSubnetDetector()
require.NoError(t, err)
s := &Server{ s := &Server{
subnetDetector: snd, privateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
} }
testCases := []struct { testCases := []struct {
want assert.BoolAssertionFunc
name string name string
cliIP net.IP cliIP net.IP
want bool
}{{ }{{
want: assert.True,
name: "local", name: "local",
cliIP: net.IP{192, 168, 0, 1}, cliIP: net.IP{192, 168, 0, 1},
want: true,
}, { }, {
want: assert.False,
name: "external", name: "external",
cliIP: net.IP{250, 249, 0, 1}, cliIP: net.IP{250, 249, 0, 1},
want: false, }, {
want: assert.False,
name: "invalid",
cliIP: net.IP{1, 2, 3, 4, 5},
}, {
want: assert.False,
name: "nil",
cliIP: nil,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@ -47,7 +53,7 @@ func TestServer_ProcessDetermineLocal(t *testing.T) {
} }
s.processDetermineLocal(dctx) s.processDetermineLocal(dctx)
assert.Equal(t, tc.want, dctx.isLocalClient) tc.want(t, dctx.isLocalClient)
}) })
} }
} }
@ -261,7 +267,7 @@ func TestServer_ProcessInternalHosts(t *testing.T) {
} }
func TestServer_ProcessRestrictLocal(t *testing.T) { func TestServer_ProcessRestrictLocal(t *testing.T) {
ups := &aghtest.TestUpstream{ ups := &aghtest.Upstream{
Reverse: map[string][]string{ Reverse: map[string][]string{
"251.252.253.254.in-addr.arpa.": {"host1.example.net."}, "251.252.253.254.in-addr.arpa.": {"host1.example.net."},
"1.1.168.192.in-addr.arpa.": {"some.local-client."}, "1.1.168.192.in-addr.arpa.": {"some.local-client."},
@ -339,7 +345,7 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) {
s := createTestServer(t, &filtering.Config{}, ServerConfig{ s := createTestServer(t, &filtering.Config{}, ServerConfig{
UDPListenAddrs: []*net.UDPAddr{{}}, UDPListenAddrs: []*net.UDPAddr{{}},
TCPListenAddrs: []*net.TCPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}},
}, &aghtest.TestUpstream{ }, &aghtest.Upstream{
Reverse: map[string][]string{ Reverse: map[string][]string{
reqAddr: {locDomain}, reqAddr: {locDomain},
}, },

View File

@ -74,7 +74,7 @@ type Server struct {
localDomainSuffix string localDomainSuffix string
ipset ipsetCtx ipset ipsetCtx
subnetDetector *aghnet.SubnetDetector privateNets netutil.SubnetSet
localResolvers *proxy.Proxy localResolvers *proxy.Proxy
sysResolvers aghnet.SystemResolvers sysResolvers aghnet.SystemResolvers
recDetector *recursionDetector recDetector *recursionDetector
@ -111,13 +111,13 @@ const defaultLocalDomainSuffix = ".lan."
// DNSCreateParams are parameters to create a new server. // DNSCreateParams are parameters to create a new server.
type DNSCreateParams struct { type DNSCreateParams struct {
DNSFilter *filtering.DNSFilter DNSFilter *filtering.DNSFilter
Stats stats.Stats Stats stats.Stats
QueryLog querylog.QueryLog QueryLog querylog.QueryLog
DHCPServer dhcpd.ServerInterface DHCPServer dhcpd.ServerInterface
SubnetDetector *aghnet.SubnetDetector PrivateNets netutil.SubnetSet
Anonymizer *aghnet.IPMut Anonymizer *aghnet.IPMut
LocalDomain string LocalDomain string
} }
// domainNameToSuffix converts a domain name into a local domain suffix. // domainNameToSuffix converts a domain name into a local domain suffix.
@ -161,7 +161,7 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
dnsFilter: p.DNSFilter, dnsFilter: p.DNSFilter,
stats: p.Stats, stats: p.Stats,
queryLog: p.QueryLog, queryLog: p.QueryLog,
subnetDetector: p.SubnetDetector, privateNets: p.PrivateNets,
localDomainSuffix: localDomainSuffix, localDomainSuffix: localDomainSuffix,
recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum), recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum),
clientIDCache: cache.New(cache.Config{ clientIDCache: cache.New(cache.Config{
@ -315,7 +315,7 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
} }
var resolver *proxy.Proxy var resolver *proxy.Proxy
if s.subnetDetector.IsLocallyServedNetwork(ip) { if s.privateNets.Contains(ip) {
if !s.conf.UsePrivateRDNS { if !s.conf.UsePrivateRDNS {
return "", nil return "", nil
} }

View File

@ -24,6 +24,7 @@ import (
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -69,14 +70,11 @@ func createTestServer(
f := filtering.New(filterConf, filters) f := filtering.New(filterConf, filters)
f.SetEnabled(true) f.SetEnabled(true)
snd, err := aghnet.NewSubnetDetector() var err error
require.NoError(t, err)
require.NotNil(t, snd)
s, err = NewServer(DNSCreateParams{ s, err = NewServer(DNSCreateParams{
DHCPServer: &testDHCP{}, DHCPServer: &testDHCP{},
DNSFilter: f, DNSFilter: f,
SubnetDetector: snd, PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -89,7 +87,7 @@ func createTestServer(
defer s.serverLock.Unlock() defer s.serverLock.Unlock()
if localUps != nil { if localUps != nil {
s.localResolvers.Config.UpstreamConfig.Upstreams = []upstream.Upstream{localUps} s.localResolvers.UpstreamConfig.Upstreams = []upstream.Upstream{localUps}
s.conf.UsePrivateRDNS = true s.conf.UsePrivateRDNS = true
} }
@ -247,7 +245,7 @@ func TestServer(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}},
}, nil) }, nil)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"google-public-dns-a.google.com.": {{8, 8, 8, 8}}, "google-public-dns-a.google.com.": {{8, 8, 8, 8}},
}, },
@ -316,7 +314,7 @@ func TestServerWithProtectionDisabled(t *testing.T) {
TCPListenAddrs: []*net.TCPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}},
}, nil) }, nil)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"google-public-dns-a.google.com.": {{8, 8, 8, 8}}, "google-public-dns-a.google.com.": {{8, 8, 8, 8}},
}, },
@ -339,7 +337,7 @@ func TestDoTServer(t *testing.T) {
TLSListenAddrs: []*net.TCPAddr{{}}, TLSListenAddrs: []*net.TCPAddr{{}},
}) })
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"google-public-dns-a.google.com.": {{8, 8, 8, 8}}, "google-public-dns-a.google.com.": {{8, 8, 8, 8}},
}, },
@ -369,7 +367,7 @@ func TestDoQServer(t *testing.T) {
QUICListenAddrs: []*net.UDPAddr{{IP: net.IP{127, 0, 0, 1}}}, QUICListenAddrs: []*net.UDPAddr{{IP: net.IP{127, 0, 0, 1}}},
}) })
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"google-public-dns-a.google.com.": {{8, 8, 8, 8}}, "google-public-dns-a.google.com.": {{8, 8, 8, 8}},
}, },
@ -413,7 +411,7 @@ func TestServerRace(t *testing.T) {
} }
s := createTestServer(t, filterConf, forwardConf, nil) s := createTestServer(t, filterConf, forwardConf, nil)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"google-public-dns-a.google.com.": {{8, 8, 8, 8}}, "google-public-dns-a.google.com.": {{8, 8, 8, 8}},
}, },
@ -552,7 +550,7 @@ func TestServerCustomClientUpstream(t *testing.T) {
} }
s := createTestServer(t, &filtering.Config{}, forwardConf, nil) s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
s.conf.GetCustomUpstreamByClient = func(_ string) (conf *proxy.UpstreamConfig, err error) { s.conf.GetCustomUpstreamByClient = func(_ string) (conf *proxy.UpstreamConfig, err error) {
ups := &aghtest.TestUpstream{ ups := &aghtest.Upstream{
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"host.": {{192, 168, 0, 1}}, "host.": {{192, 168, 0, 1}},
}, },
@ -580,9 +578,9 @@ func TestServerCustomClientUpstream(t *testing.T) {
} }
// testCNAMEs is a map of names and CNAMEs necessary for the TestUpstream work. // testCNAMEs is a map of names and CNAMEs necessary for the TestUpstream work.
var testCNAMEs = map[string]string{ var testCNAMEs = map[string][]string{
"badhost.": "NULL.example.org.", "badhost.": {"NULL.example.org."},
"whitelist.example.org.": "NULL.example.org.", "whitelist.example.org.": {"NULL.example.org."},
} }
// testIPv4 is a map of names and IPv4s necessary for the TestUpstream work. // testIPv4 is a map of names and IPv4s necessary for the TestUpstream work.
@ -596,7 +594,7 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) {
UDPListenAddrs: []*net.UDPAddr{{}}, UDPListenAddrs: []*net.UDPAddr{{}},
TCPListenAddrs: []*net.TCPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}},
}, nil) }, nil)
testUpstm := &aghtest.TestUpstream{ testUpstm := &aghtest.Upstream{
CName: testCNAMEs, CName: testCNAMEs,
IPv4: testIPv4, IPv4: testIPv4,
IPv6: nil, IPv6: nil,
@ -630,7 +628,7 @@ func TestBlockCNAME(t *testing.T) {
} }
s := createTestServer(t, &filtering.Config{}, forwardConf, nil) s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
CName: testCNAMEs, CName: testCNAMEs,
IPv4: testIPv4, IPv4: testIPv4,
}, },
@ -640,14 +638,17 @@ func TestBlockCNAME(t *testing.T) {
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String() addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
testCases := []struct { testCases := []struct {
name string
host string host string
want bool want bool
}{{ }{{
name: "block_request",
host: "badhost.", host: "badhost.",
// 'badhost' has a canonical name 'NULL.example.org' which is // 'badhost' has a canonical name 'NULL.example.org' which is
// blocked by filters: response is blocked. // blocked by filters: response is blocked.
want: true, want: true,
}, { }, {
name: "allowed",
host: "whitelist.example.org.", host: "whitelist.example.org.",
// 'whitelist.example.org' has a canonical name // 'whitelist.example.org' has a canonical name
// 'NULL.example.org' which is blocked by filters // 'NULL.example.org' which is blocked by filters
@ -655,6 +656,7 @@ func TestBlockCNAME(t *testing.T) {
// response isn't blocked. // response isn't blocked.
want: false, want: false,
}, { }, {
name: "block_response",
host: "example.org.", host: "example.org.",
// 'example.org' has a canonical name 'cname1' with IP // 'example.org' has a canonical name 'cname1' with IP
// 127.0.0.255 which is blocked by filters: response is blocked. // 127.0.0.255 which is blocked by filters: response is blocked.
@ -662,9 +664,9 @@ func TestBlockCNAME(t *testing.T) {
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run("block_cname_"+tc.host, func(t *testing.T) { req := createTestMessage(tc.host)
req := createTestMessage(tc.host)
t.Run(tc.name, func(t *testing.T) {
reply, err := dns.Exchange(req, addr) reply, err := dns.Exchange(req, addr)
require.NoError(t, err) require.NoError(t, err)
@ -674,7 +676,7 @@ func TestBlockCNAME(t *testing.T) {
ans := reply.Answer[0] ans := reply.Answer[0]
a, ok := ans.(*dns.A) a, ok := ans.(*dns.A)
require.Truef(t, ok, "got %T", ans) require.True(t, ok)
assert.True(t, a.A.IsUnspecified()) assert.True(t, a.A.IsUnspecified())
} }
@ -695,7 +697,7 @@ func TestClientRulesForCNAMEMatching(t *testing.T) {
} }
s := createTestServer(t, &filtering.Config{}, forwardConf, nil) s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
CName: testCNAMEs, CName: testCNAMEs,
IPv4: testIPv4, IPv4: testIPv4,
}, },
@ -766,16 +768,11 @@ func TestBlockedCustomIP(t *testing.T) {
Data: []byte(rules), Data: []byte(rules),
}} }}
snd, err := aghnet.NewSubnetDetector()
require.NoError(t, err)
require.NotNil(t, snd)
f := filtering.New(&filtering.Config{}, filters) f := filtering.New(&filtering.Config{}, filters)
var s *Server s, err := NewServer(DNSCreateParams{
s, err = NewServer(DNSCreateParams{ DHCPServer: &testDHCP{},
DHCPServer: &testDHCP{}, DNSFilter: f,
DNSFilter: f, PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
SubnetDetector: snd,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -909,15 +906,10 @@ func TestRewrite(t *testing.T) {
f := filtering.New(c, nil) f := filtering.New(c, nil)
f.SetEnabled(true) f.SetEnabled(true)
snd, err := aghnet.NewSubnetDetector() s, err := NewServer(DNSCreateParams{
require.NoError(t, err) DHCPServer: &testDHCP{},
require.NotNil(t, snd) DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
var s *Server
s, err = NewServer(DNSCreateParams{
DHCPServer: &testDHCP{},
DNSFilter: f,
SubnetDetector: snd,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -931,9 +923,9 @@ func TestRewrite(t *testing.T) {
})) }))
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.TestUpstream{ &aghtest.Upstream{
CName: map[string]string{ CName: map[string][]string{
"example.org": "somename", "example.org": {"somename"},
}, },
IPv4: map[string][]net.IP{ IPv4: map[string][]net.IP{
"example.org.": {{4, 3, 2, 1}}, "example.org.": {{4, 3, 2, 1}},
@ -1024,15 +1016,10 @@ func (d *testDHCP) Leases(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {} func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {}
func TestPTRResponseFromDHCPLeases(t *testing.T) { func TestPTRResponseFromDHCPLeases(t *testing.T) {
snd, err := aghnet.NewSubnetDetector() s, err := NewServer(DNSCreateParams{
require.NoError(t, err) DNSFilter: filtering.New(&filtering.Config{}, nil),
require.NotNil(t, snd) DHCPServer: &testDHCP{},
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
var s *Server
s, err = NewServer(DNSCreateParams{
DNSFilter: filtering.New(&filtering.Config{}, nil),
DHCPServer: &testDHCP{},
SubnetDetector: snd,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -1101,16 +1088,11 @@ func TestPTRResponseFromHosts(t *testing.T) {
}, nil) }, nil)
flt.SetEnabled(true) flt.SetEnabled(true)
var snd *aghnet.SubnetDetector
snd, err = aghnet.NewSubnetDetector()
require.NoError(t, err)
require.NotNil(t, snd)
var s *Server var s *Server
s, err = NewServer(DNSCreateParams{ s, err = NewServer(DNSCreateParams{
DHCPServer: &testDHCP{}, DHCPServer: &testDHCP{},
DNSFilter: flt, DNSFilter: flt,
SubnetDetector: snd, PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -1193,12 +1175,12 @@ func TestNewServer(t *testing.T) {
} }
func TestServer_Exchange(t *testing.T) { func TestServer_Exchange(t *testing.T) {
extUpstream := &aghtest.TestUpstream{ extUpstream := &aghtest.Upstream{
Reverse: map[string][]string{ Reverse: map[string][]string{
"1.1.1.1.in-addr.arpa.": {"one.one.one.one"}, "1.1.1.1.in-addr.arpa.": {"one.one.one.one"},
}, },
} }
locUpstream := &aghtest.TestUpstream{ locUpstream := &aghtest.Upstream{
Reverse: map[string][]string{ Reverse: map[string][]string{
"1.1.168.192.in-addr.arpa.": {"local.domain"}, "1.1.168.192.in-addr.arpa.": {"local.domain"},
"2.1.168.192.in-addr.arpa.": {}, "2.1.168.192.in-addr.arpa.": {},
@ -1223,9 +1205,7 @@ func TestServer_Exchange(t *testing.T) {
srv.conf.ResolveClients = true srv.conf.ResolveClients = true
srv.conf.UsePrivateRDNS = true srv.conf.UsePrivateRDNS = true
var err error srv.privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
srv.subnetDetector, err = aghnet.NewSubnetDetector()
require.NoError(t, err)
localIP := net.IP{192, 168, 1, 1} localIP := net.IP{192, 168, 1, 1}
testCases := []struct { testCases := []struct {

View File

@ -116,7 +116,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*filtering.Result, error) {
// 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(host string, qtype uint16, setts *filtering.Settings) ( func (s *Server) checkHostRules(host string, rrtype uint16, setts *filtering.Settings) (
r *filtering.Result, r *filtering.Result,
err error, err error,
) { ) {
@ -128,7 +128,7 @@ func (s *Server) checkHostRules(host string, qtype uint16, setts *filtering.Sett
} }
var res filtering.Result var res filtering.Result
res, err = s.dnsFilter.CheckHostRules(host, qtype, setts) res, err = s.dnsFilter.CheckHostRules(host, rrtype, setts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -136,33 +136,36 @@ func (s *Server) checkHostRules(host string, qtype uint16, setts *filtering.Sett
return &res, err return &res, err
} }
// If response contains CNAME, A or AAAA records, we apply filtering to each // filterDNSResponse checks each resource record of the response's answer
// canonical host name or IP address. If this is a match, we set a new response // section from ctx and returns a non-nil res if at least one of canonnical
// in d.Res and return. // names or IP addresses in it matches the filtering rules.
func (s *Server) filterDNSResponse(ctx *dnsContext) (*filtering.Result, error) { func (s *Server) filterDNSResponse(ctx *dnsContext) (res *filtering.Result, err error) {
d := ctx.proxyCtx d := ctx.proxyCtx
setts := ctx.setts
if !setts.FilteringEnabled {
return nil, nil
}
for _, a := range d.Res.Answer { for _, a := range d.Res.Answer {
host := "" host := ""
var rrtype uint16
switch v := a.(type) { switch a := a.(type) {
case *dns.CNAME: case *dns.CNAME:
log.Debug("DNSFwd: Checking CNAME %s for %s", v.Target, v.Hdr.Name) host = strings.TrimSuffix(a.Target, ".")
host = strings.TrimSuffix(v.Target, ".") rrtype = dns.TypeCNAME
case *dns.A: case *dns.A:
host = v.A.String() host = a.A.String()
log.Debug("DNSFwd: Checking record A (%s) for %s", host, v.Hdr.Name) rrtype = dns.TypeA
case *dns.AAAA: case *dns.AAAA:
host = v.AAAA.String() host = a.AAAA.String()
log.Debug("DNSFwd: Checking record AAAA (%s) for %s", host, v.Hdr.Name) rrtype = dns.TypeAAAA
default: default:
continue continue
} }
host = strings.TrimSuffix(host, ".") log.Debug("dnsforward: checking %s %s for %s", dns.Type(rrtype), host, a.Header().Name)
res, err := s.checkHostRules(host, d.Req.Question[0].Qtype, ctx.setts)
res, err = s.checkHostRules(host, rrtype, setts)
if err != nil { if err != nil {
return nil, err return nil, err
} else if res == nil { } else if res == nil {

View File

@ -0,0 +1,154 @@
package dnsforward
import (
"net"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
rules := `
||blocked.domain^
@@||allowed.domain^
||cname.specific^$dnstype=~CNAME
||0.0.0.1^$dnstype=~A
||::1^$dnstype=~AAAA
`
forwardConf := ServerConfig{
UDPListenAddrs: []*net.UDPAddr{{}},
TCPListenAddrs: []*net.TCPAddr{{}},
FilteringConfig: FilteringConfig{
ProtectionEnabled: true,
BlockingMode: BlockingModeDefault,
},
}
filters := []filtering.Filter{{
ID: 0, Data: []byte(rules),
}}
f := filtering.New(&filtering.Config{}, filters)
f.SetEnabled(true)
s, err := NewServer(DNSCreateParams{
DHCPServer: &testDHCP{},
DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
})
require.NoError(t, err)
s.conf = forwardConf
err = s.Prepare(nil)
require.NoError(t, err)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.Upstream{
CName: map[string][]string{
"cname.exception.": {"cname.specific."},
"should.block.": {"blocked.domain."},
"allowed.first.": {"allowed.domain.", "blocked.domain."},
"blocked.first.": {"blocked.domain.", "allowed.domain."},
},
IPv4: map[string][]net.IP{
"a.exception.": {{0, 0, 0, 1}},
},
IPv6: map[string][]net.IP{
"aaaa.exception.": {net.ParseIP("::1")},
},
},
}
startDeferStop(t, s)
testCases := []struct {
req *dns.Msg
name string
wantAns []dns.RR
}{{
req: createTestMessage("cname.exception."),
name: "cname_exception",
wantAns: []dns.RR{&dns.CNAME{
Hdr: dns.RR_Header{
Name: "cname.exception.",
Rrtype: dns.TypeCNAME,
},
Target: "cname.specific.",
}},
}, {
req: createTestMessage("should.block."),
name: "blocked_by_cname",
wantAns: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "should.block.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: netutil.IPv4Zero(),
}},
}, {
req: createTestMessage("a.exception."),
name: "a_exception",
wantAns: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "a.exception.",
Rrtype: dns.TypeA,
},
A: net.IP{0, 0, 0, 1},
}},
}, {
req: createTestMessageWithType("aaaa.exception.", dns.TypeAAAA),
name: "aaaa_exception",
wantAns: []dns.RR{&dns.AAAA{
Hdr: dns.RR_Header{
Name: "aaaa.exception.",
Rrtype: dns.TypeAAAA,
},
AAAA: net.ParseIP("::1"),
}},
}, {
req: createTestMessage("allowed.first."),
name: "allowed_first",
wantAns: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "allowed.first.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: netutil.IPv4Zero(),
}},
}, {
req: createTestMessage("blocked.first."),
name: "blocked_first",
wantAns: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "blocked.first.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: netutil.IPv4Zero(),
}},
}}
for _, tc := range testCases {
dctx := &proxy.DNSContext{
Proto: proxy.ProtoUDP,
Req: tc.req,
Addr: &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 1},
}
t.Run(tc.name, func(t *testing.T) {
err = s.handleDNSRequest(nil, dctx)
require.NoError(t, err)
require.NotNil(t, dctx.Res)
assert.Equal(t, tc.wantAns, dctx.Res.Answer)
})
}
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"sort"
"strings" "strings"
"time" "time"
@ -41,7 +42,7 @@ type dnsConfig struct {
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"` LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
} }
func (s *Server) getDNSConfig() dnsConfig { func (s *Server) getDNSConfig() (c *dnsConfig) {
s.serverLock.RLock() s.serverLock.RLock()
defer s.serverLock.RUnlock() defer s.serverLock.RUnlock()
@ -70,7 +71,7 @@ func (s *Server) getDNSConfig() dnsConfig {
upstreamMode = "parallel" upstreamMode = "parallel"
} }
return dnsConfig{ return &dnsConfig{
Upstreams: &upstreams, Upstreams: &upstreams,
UpstreamsFile: &upstreamFile, UpstreamsFile: &upstreamFile,
Bootstraps: &bootstraps, Bootstraps: &bootstraps,
@ -106,7 +107,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
// since there is no need to omit it while decoding from JSON. // since there is no need to omit it while decoding from JSON.
DefautLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"` DefautLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"`
}{ }{
dnsConfig: s.getDNSConfig(), dnsConfig: *s.getDNSConfig(),
DefautLocalPTRUpstreams: defLocalPTRUps, DefautLocalPTRUpstreams: defLocalPTRUps,
} }
@ -138,39 +139,63 @@ func (req *dnsConfig) checkBlockingMode() bool {
} }
func (req *dnsConfig) checkUpstreamsMode() bool { func (req *dnsConfig) checkUpstreamsMode() bool {
if req.UpstreamMode == nil { valid := []string{"", "fastest_addr", "parallel"}
return true
}
for _, valid := range []string{ return req.UpstreamMode == nil || stringutil.InSlice(valid, *req.UpstreamMode)
"",
"fastest_addr",
"parallel",
} {
if *req.UpstreamMode == valid {
return true
}
}
return false
} }
func (req *dnsConfig) checkBootstrap() (string, error) { func (req *dnsConfig) checkBootstrap() (err error) {
if req.Bootstraps == nil { if req.Bootstraps == nil {
return "", nil return nil
} }
for _, boot := range *req.Bootstraps { var b string
if boot == "" { defer func() { err = errors.Annotate(err, "checking bootstrap %s: invalid address: %w", b) }()
return boot, fmt.Errorf("invalid bootstrap server address: empty")
for _, b = range *req.Bootstraps {
if b == "" {
return errors.Error("empty")
} }
if _, err := upstream.NewResolver(boot, nil); err != nil { if _, err = upstream.NewResolver(b, nil); err != nil {
return boot, fmt.Errorf("invalid bootstrap server address: %w", err) return err
} }
} }
return "", nil return nil
}
// validate returns an error if any field of req is invalid.
func (req *dnsConfig) validate(privateNets netutil.SubnetSet) (err error) {
if req.Upstreams != nil {
err = ValidateUpstreams(*req.Upstreams)
if err != nil {
return fmt.Errorf("validating upstream servers: %w", err)
}
}
if req.LocalPTRUpstreams != nil {
err = ValidateUpstreamsPrivate(*req.LocalPTRUpstreams, privateNets)
if err != nil {
return fmt.Errorf("validating private upstream servers: %w", err)
}
}
err = req.checkBootstrap()
if err != nil {
return err
}
switch {
case !req.checkBlockingMode():
return errors.Error("blocking_mode: incorrect value")
case !req.checkUpstreamsMode():
return errors.Error("upstream_mode: incorrect value")
case !req.checkCacheTTL():
return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max")
default:
return nil
}
} }
func (req *dnsConfig) checkCacheTTL() bool { func (req *dnsConfig) checkCacheTTL() bool {
@ -190,69 +215,33 @@ func (req *dnsConfig) checkCacheTTL() bool {
} }
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
req := dnsConfig{} req := &dnsConfig{}
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json Encode: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "decoding request: %s", err)
return return
} }
if req.Upstreams != nil { err = req.validate(s.privateNets)
if err = ValidateUpstreams(*req.Upstreams); err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
}
}
var errBoot string
if errBoot, err = req.checkBootstrap(); err != nil {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"%s can not be used as bootstrap dns cause: %s",
errBoot,
err,
)
return return
} }
switch {
case !req.checkBlockingMode():
aghhttp.Error(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
return
case !req.checkUpstreamsMode():
aghhttp.Error(r, w, http.StatusBadRequest, "upstream_mode: incorrect value")
return
case !req.checkCacheTTL():
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"cache_ttl_min must be less or equal than cache_ttl_max",
)
return
default:
// Go on.
}
restart := s.setConfig(req) restart := s.setConfig(req)
s.conf.ConfigModified() s.conf.ConfigModified()
if restart { if restart {
if err = s.Reconfigure(nil); err != nil { err = s.Reconfigure(nil)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err) aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
} }
} }
} }
func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) { func (s *Server) setConfigRestartable(dc *dnsConfig) (restart bool) {
if dc.Upstreams != nil { if dc.Upstreams != nil {
s.conf.UpstreamDNS = *dc.Upstreams s.conf.UpstreamDNS = *dc.Upstreams
restart = true restart = true
@ -273,9 +262,9 @@ func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) {
restart = true restart = true
} }
if dc.RateLimit != nil { if dc.RateLimit != nil && s.conf.Ratelimit != *dc.RateLimit {
restart = restart || s.conf.Ratelimit != *dc.RateLimit
s.conf.Ratelimit = *dc.RateLimit s.conf.Ratelimit = *dc.RateLimit
restart = true
} }
if dc.EDNSCSEnabled != nil { if dc.EDNSCSEnabled != nil {
@ -306,7 +295,7 @@ func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) {
return restart return restart
} }
func (s *Server) setConfig(dc dnsConfig) (restart bool) { func (s *Server) setConfig(dc *dnsConfig) (restart bool) {
s.serverLock.Lock() s.serverLock.Lock()
defer s.serverLock.Unlock() defer s.serverLock.Unlock()
@ -353,52 +342,106 @@ type upstreamJSON struct {
PrivateUpstreams []string `json:"private_upstream"` PrivateUpstreams []string `json:"private_upstream"`
} }
// IsCommentOrEmpty returns true of the string starts with a "#" character or is // IsCommentOrEmpty returns true if s starts with a "#" character or is empty.
// an empty string. This function is useful for filtering out non-upstream // This function is useful for filtering out non-upstream lines from upstream
// lines from upstream configs. // configs.
func IsCommentOrEmpty(s string) (ok bool) { func IsCommentOrEmpty(s string) (ok bool) {
return len(s) == 0 || s[0] == '#' return len(s) == 0 || s[0] == '#'
} }
// newUpstreamConfig validates upstreams and returns an appropriate upstream
// configuration or nil if it can't be built.
//
// TODO(e.burkov): Perhaps proxy.ParseUpstreamsConfig should validate upstreams
// slice already so that this function may be considered useless.
func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err error) {
// No need to validate comments and empty lines.
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
if len(upstreams) == 0 {
// Consider this case valid since it means the default server should be
// used.
return nil, nil
}
conf, err = proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
)
if err != nil {
return nil, err
} else if len(conf.Upstreams) == 0 {
return nil, errors.Error("no default upstreams specified")
}
for _, u := range upstreams {
_, err = validateUpstream(u)
if err != nil {
return nil, err
}
}
return conf, nil
}
// ValidateUpstreams validates each upstream and returns an error if any // ValidateUpstreams validates each upstream and returns an error if any
// upstream is invalid or if there are no default upstreams specified. // upstream is invalid or if there are no default upstreams specified.
// //
// TODO(e.burkov): Move into aghnet or even into dnsproxy. // TODO(e.burkov): Move into aghnet or even into dnsproxy.
func ValidateUpstreams(upstreams []string) (err error) { func ValidateUpstreams(upstreams []string) (err error) {
// No need to validate comments _, err = newUpstreamConfig(upstreams)
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
// Consider this case valid because defaultDNS will be used return err
if len(upstreams) == 0 { }
return nil
// stringKeysSorted returns the sorted slice of string keys of m.
//
// TODO(e.burkov): Use generics in Go 1.18. Move into golibs.
func stringKeysSorted(m map[string][]upstream.Upstream) (sorted []string) {
sorted = make([]string, 0, len(m))
for s := range m {
sorted = append(sorted, s)
} }
_, err = proxy.ParseUpstreamsConfig( sort.Strings(sorted)
upstreams,
&upstream.Options{ return sorted
Bootstrap: []string{}, }
Timeout: DefaultTimeout,
}, // ValidateUpstreamsPrivate validates each upstream and returns an error if any
) // upstream is invalid or if there are no default upstreams specified. It also
// checks each domain of domain-specific upstreams for being ARPA pointing to
// a locally-served network. privateNets must not be nil.
func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) (err error) {
conf, err := newUpstreamConfig(upstreams)
if err != nil { if err != nil {
return err return err
} }
var defaultUpstreamFound bool if conf == nil {
for _, u := range upstreams { return nil
var useDefault bool }
useDefault, err = validateUpstream(u)
var errs []error
for _, domain := range stringKeysSorted(conf.DomainReservedUpstreams) {
var subnet *net.IPNet
subnet, err = netutil.SubnetFromReversedAddr(domain)
if err != nil { if err != nil {
return err errs = append(errs, err)
continue
} }
if !defaultUpstreamFound { if !privateNets.Contains(subnet.IP) {
defaultUpstreamFound = useDefault errs = append(
errs,
fmt.Errorf("arpa domain %q should point to a locally-served network", domain),
)
} }
} }
if !defaultUpstreamFound { if len(errs) > 0 {
return fmt.Errorf("no default upstreams specified") return errors.List("checking domain-specific upstreams", errs...)
} }
return nil return nil

View File

@ -14,6 +14,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -184,12 +185,11 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
wantSet: "", wantSet: "",
}, { }, {
name: "upstream_dns_bad", name: "upstream_dns_bad",
wantSet: `wrong upstreams specification: bad ipport address "!!!": address !!!: ` + wantSet: `validating upstream servers: bad ipport address "!!!": ` +
`missing port in address`, `address !!!: missing port in address`,
}, { }, {
name: "bootstraps_bad", name: "bootstraps_bad",
wantSet: `a can not be used as bootstrap dns cause: ` + wantSet: `checking bootstrap a: invalid address: ` +
`invalid bootstrap server address: ` +
`Resolver a is not eligible to be a bootstrap DNS server`, `Resolver a is not eligible to be a bootstrap DNS server`,
}, { }, {
name: "cache_bad_ttl", name: "cache_bad_ttl",
@ -200,6 +200,10 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
}, { }, {
name: "local_ptr_upstreams_good", name: "local_ptr_upstreams_good",
wantSet: "", wantSet: "",
}, {
name: "local_ptr_upstreams_bad",
wantSet: `validating private upstream servers: checking domain-specific upstreams: ` +
`bad arpa domain name "non.arpa": not a reversed ip network`,
}, { }, {
name: "local_ptr_upstreams_null", name: "local_ptr_upstreams_null",
wantSet: "", wantSet: "",
@ -358,7 +362,7 @@ func TestValidateUpstream(t *testing.T) {
} }
} }
func TestValidateUpstreamsSet(t *testing.T) { func TestValidateUpstreams(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
wantErr string wantErr string
@ -405,3 +409,51 @@ func TestValidateUpstreamsSet(t *testing.T) {
}) })
} }
} }
func TestValidateUpstreamsPrivate(t *testing.T) {
ss := netutil.SubnetSetFunc(netutil.IsLocallyServed)
testCases := []struct {
name string
wantErr string
u string
}{{
name: "success_address",
wantErr: ``,
u: "[/1.0.0.127.in-addr.arpa/]#",
}, {
name: "success_subnet",
wantErr: ``,
u: "[/127.in-addr.arpa/]#",
}, {
name: "not_arpa_subnet",
wantErr: `checking domain-specific upstreams: ` +
`bad arpa domain name "hello.world": not a reversed ip network`,
u: "[/hello.world/]#",
}, {
name: "non-private_arpa_address",
wantErr: `checking domain-specific upstreams: ` +
`arpa domain "1.2.3.4.in-addr.arpa." should point to a locally-served network`,
u: "[/1.2.3.4.in-addr.arpa/]#",
}, {
name: "non-private_arpa_subnet",
wantErr: `checking domain-specific upstreams: ` +
`arpa domain "128.in-addr.arpa." should point to a locally-served network`,
u: "[/128.in-addr.arpa/]#",
}, {
name: "several_bad",
wantErr: `checking domain-specific upstreams: 2 errors: ` +
`"arpa domain \"1.2.3.4.in-addr.arpa.\" should point to a locally-served network", ` +
`"bad arpa domain name \"non.arpa\": not a reversed ip network"`,
u: "[/non.arpa/1.2.3.4.in-addr.arpa/127.in-addr.arpa/]#",
}}
for _, tc := range testCases {
set := []string{"192.168.0.1", tc.u}
t.Run(tc.name, func(t *testing.T) {
err := ValidateUpstreamsPrivate(set, ss)
testutil.AssertErrorMsg(t, tc.wantErr, err)
})
}
}

View File

@ -520,6 +520,43 @@
] ]
} }
}, },
"local_ptr_upstreams_bad": {
"req": {
"local_ptr_upstreams": [
"123.123.123.123",
"[/non.arpa/]#"
]
},
"want": {
"upstream_dns": [
"8.8.8.8:53",
"8.8.4.4:53"
],
"upstream_dns_file": "",
"bootstrap_dns": [
"9.9.9.10",
"149.112.112.10",
"2620:fe::10",
"2620:fe::fe:10"
],
"protection_enabled": true,
"ratelimit": 0,
"blocking_mode": "",
"blocking_ipv4": "",
"blocking_ipv6": "",
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
"upstream_mode": "",
"cache_size": 0,
"cache_ttl_min": 0,
"cache_ttl_max": 0,
"cache_optimistic": false,
"resolve_clients": false,
"use_private_ptr_resolvers": false,
"local_ptr_upstreams": []
}
},
"local_ptr_upstreams_null": { "local_ptr_upstreams_null": {
"req": { "req": {
"local_ptr_upstreams": null "local_ptr_upstreams": null

View File

@ -420,14 +420,8 @@ func (r Reason) Matched() bool {
} }
// CheckHostRules tries to match the host against filtering rules only. // CheckHostRules tries to match the host against filtering rules only.
func (d *DNSFilter) CheckHostRules(host string, qtype uint16, setts *Settings) (Result, error) { func (d *DNSFilter) CheckHostRules(host string, rrtype uint16, setts *Settings) (Result, error) {
if !setts.FilteringEnabled { return d.matchHost(strings.ToLower(host), rrtype, setts)
return Result{}, nil
}
host = strings.ToLower(host)
return d.matchHost(host, qtype, setts)
} }
// CheckHost tries to match the host against filtering rules, then safebrowsing // CheckHost tries to match the host against filtering rules, then safebrowsing
@ -726,8 +720,7 @@ func hostRulesToRules(netRules []*rules.HostRule) (res []rules.Rule) {
return res return res
} }
// matchHostProcessAllowList processes the allowlist logic of host // matchHostProcessAllowList processes the allowlist logic of host matching.
// matching.
func (d *DNSFilter) matchHostProcessAllowList( func (d *DNSFilter) matchHostProcessAllowList(
host string, host string,
dnsres *urlfilter.DNSResult, dnsres *urlfilter.DNSResult,
@ -798,11 +791,11 @@ func (d *DNSFilter) matchHostProcessDNSResult(
return Result{} return Result{}
} }
// matchHost is a low-level way to check only if hostname is filtered by rules, // matchHost is a low-level way to check only if host is filtered by rules,
// skipping expensive safebrowsing and parental lookups. // skipping expensive safebrowsing and parental lookups.
func (d *DNSFilter) matchHost( func (d *DNSFilter) matchHost(
host string, host string,
qtype uint16, rrtype uint16,
setts *Settings, setts *Settings,
) (res Result, err error) { ) (res Result, err error) {
if !setts.FilteringEnabled { if !setts.FilteringEnabled {
@ -815,7 +808,7 @@ func (d *DNSFilter) matchHost(
// TODO(e.burkov): Wait for urlfilter update to pass net.IP. // TODO(e.burkov): Wait for urlfilter update to pass net.IP.
ClientIP: setts.ClientIP.String(), ClientIP: setts.ClientIP.String(),
ClientName: setts.ClientName, ClientName: setts.ClientName,
DNSType: qtype, DNSType: rrtype,
} }
d.engineLock.RLock() d.engineLock.RLock()
@ -855,7 +848,7 @@ func (d *DNSFilter) matchHost(
return Result{}, nil return Result{}, nil
} }
res = d.matchHostProcessDNSResult(qtype, dnsres) res = d.matchHostProcessDNSResult(rrtype, dnsres)
for _, r := range res.Rules { for _, r := range res.Rules {
log.Debug( log.Debug(
"filtering: found rule %q for host %q, filter list id: %d", "filtering: found rule %q for host %q, filter list id: %d",

View File

@ -4,8 +4,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"os/exec"
"runtime"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -62,6 +60,16 @@ const (
ClientSourceHostsFile ClientSourceHostsFile
) )
// clientSourceConf is used to configure where the runtime clients will be
// obtained from.
type clientSourcesConf struct {
WHOIS bool `yaml:"whois"`
ARP bool `yaml:"arp"`
RDNS bool `yaml:"rdns"`
DHCP bool `yaml:"dhcp"`
HostsFile bool `yaml:"hosts"`
}
// RuntimeClient information // RuntimeClient information
type RuntimeClient struct { type RuntimeClient struct {
WHOISInfo *RuntimeClientWHOISInfo WHOISInfo *RuntimeClientWHOISInfo
@ -99,6 +107,9 @@ type clientsContainer struct {
// hosts database. // hosts database.
etcHosts *aghnet.HostsContainer etcHosts *aghnet.HostsContainer
// arpdb stores the neighbors retrieved from ARP.
arpdb aghnet.ARPDB
testing bool // if TRUE, this object is used for internal tests testing bool // if TRUE, this object is used for internal tests
} }
@ -109,6 +120,7 @@ func (clients *clientsContainer) Init(
objects []*clientObject, objects []*clientObject,
dhcpServer *dhcpd.Server, dhcpServer *dhcpd.Server,
etcHosts *aghnet.HostsContainer, etcHosts *aghnet.HostsContainer,
arpdb aghnet.ARPDB,
) { ) {
if clients.list != nil { if clients.list != nil {
log.Fatal("clients.list != nil") log.Fatal("clients.list != nil")
@ -121,6 +133,7 @@ func (clients *clientsContainer) Init(
clients.dhcpServer = dhcpServer clients.dhcpServer = dhcpServer
clients.etcHosts = etcHosts clients.etcHosts = etcHosts
clients.arpdb = arpdb
clients.addFromConfig(objects) clients.addFromConfig(objects)
if clients.testing { if clients.testing {
@ -132,14 +145,14 @@ func (clients *clientsContainer) Init(
clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged) clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged)
} }
go clients.handleHostsUpdates() if clients.etcHosts != nil {
go clients.handleHostsUpdates()
}
} }
func (clients *clientsContainer) handleHostsUpdates() { func (clients *clientsContainer) handleHostsUpdates() {
if clients.etcHosts != nil { for upd := range clients.etcHosts.Upd() {
for upd := range clients.etcHosts.Upd() { clients.addFromHostsFile(upd)
clients.addFromHostsFile(upd)
}
} }
} }
@ -156,7 +169,9 @@ func (clients *clientsContainer) Start() {
// Reload reloads runtime clients. // Reload reloads runtime clients.
func (clients *clientsContainer) Reload() { func (clients *clientsContainer) Reload() {
clients.addFromSystemARP() if clients.arpdb != nil {
clients.addFromSystemARP()
}
} }
type clientObject struct { type clientObject struct {
@ -255,6 +270,8 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
} }
func (clients *clientsContainer) periodicUpdate() { func (clients *clientsContainer) periodicUpdate() {
defer log.OnPanic("clients container")
for { for {
clients.Reload() clients.Reload()
time.Sleep(clientsUpdatePeriod) time.Sleep(clientsUpdatePeriod)
@ -733,6 +750,7 @@ func (clients *clientsContainer) addHostLocked(ip net.IP, host string, src clien
return false return false
} }
rc.Host = host
rc.Source = src rc.Source = src
} else { } else {
rc = &RuntimeClient{ rc = &RuntimeClient{
@ -805,16 +823,18 @@ func (clients *clientsContainer) addFromHostsFile(hosts *netutil.IPMap) {
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a // addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
// command. // command.
func (clients *clientsContainer) addFromSystemARP() { func (clients *clientsContainer) addFromSystemARP() {
if runtime.GOOS == "windows" { if err := clients.arpdb.Refresh(); err != nil {
log.Error("refreshing arp container: %s", err)
clients.arpdb = aghnet.EmptyARPDB{}
return return
} }
cmd := exec.Command("arp", "-a") ns := clients.arpdb.Neighbors()
log.Tracef("executing %q %q", cmd.Path, cmd.Args) if len(ns) == 0 {
data, err := cmd.Output() log.Debug("refreshing arp container: the update is empty")
if err != nil || cmd.ProcessState.ExitCode() != 0 {
log.Debug("command %q has failed: %q code:%d",
cmd.Path, err, cmd.ProcessState.ExitCode())
return return
} }
@ -823,36 +843,20 @@ func (clients *clientsContainer) addFromSystemARP() {
clients.rmHostsBySrc(ClientSourceARP) clients.rmHostsBySrc(ClientSourceARP)
n := 0 added := 0
// TODO(a.garipov): Rewrite to use bufio.Scanner. for _, n := range ns {
lines := strings.Split(string(data), "\n") if clients.addHostLocked(n.IP, n.Name, ClientSourceARP) {
for _, ln := range lines { added++
lparen := strings.Index(ln, " (")
rparen := strings.Index(ln, ") ")
if lparen == -1 || rparen == -1 || lparen >= rparen {
continue
}
host := ln[:lparen]
ipStr := ln[lparen+2 : rparen]
ip := net.ParseIP(ipStr)
if netutil.ValidateDomainName(host) != nil || ip == nil {
continue
}
ok := clients.addHostLocked(ip, host, ClientSourceARP)
if ok {
n++
} }
} }
log.Debug("clients: added %d client aliases from 'arp -a' command output", n) log.Debug("clients: added %d client aliases from arp neighborhood", added)
} }
// updateFromDHCP adds the clients that have a non-empty hostname from the DHCP // updateFromDHCP adds the clients that have a non-empty hostname from the DHCP
// server. // server.
func (clients *clientsContainer) updateFromDHCP(add bool) { func (clients *clientsContainer) updateFromDHCP(add bool) {
if clients.dhcpServer == nil { if clients.dhcpServer == nil || !config.Clients.Sources.DHCP {
return return
} }

View File

@ -3,11 +3,12 @@ package home
import ( import (
"net" "net"
"os" "os"
"runtime"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -16,7 +17,7 @@ func TestClients(t *testing.T) {
clients := clientsContainer{} clients := clientsContainer{}
clients.testing = true clients.testing = true
clients.Init(nil, nil, nil) clients.Init(nil, nil, nil, nil)
t.Run("add_success", func(t *testing.T) { t.Run("add_success", func(t *testing.T) {
c := &Client{ c := &Client{
@ -192,7 +193,7 @@ func TestClientsWHOIS(t *testing.T) {
clients := clientsContainer{ clients := clientsContainer{
testing: true, testing: true,
} }
clients.Init(nil, nil, nil) clients.Init(nil, nil, nil, nil)
whois := &RuntimeClientWHOISInfo{ whois := &RuntimeClientWHOISInfo{
Country: "AU", Country: "AU",
Orgname: "Example Org", Orgname: "Example Org",
@ -251,7 +252,7 @@ func TestClientsAddExisting(t *testing.T) {
clients := clientsContainer{ clients := clientsContainer{
testing: true, testing: true,
} }
clients.Init(nil, nil, nil) clients.Init(nil, nil, nil, nil)
t.Run("simple", func(t *testing.T) { t.Run("simple", func(t *testing.T) {
ip := net.IP{1, 1, 1, 1} ip := net.IP{1, 1, 1, 1}
@ -271,12 +272,18 @@ func TestClientsAddExisting(t *testing.T) {
}) })
t.Run("complicated", func(t *testing.T) { t.Run("complicated", func(t *testing.T) {
// TODO(a.garipov): Properly decouple the DHCP server from the client
// storage.
if runtime.GOOS == "windows" {
t.Skip("skipping dhcp test on windows")
}
var err error var err error
ip := net.IP{1, 2, 3, 4} ip := net.IP{1, 2, 3, 4}
// First, init a DHCP server with a single static lease. // First, init a DHCP server with a single static lease.
config := dhcpd.ServerConfig{ config := &dhcpd.ServerConfig{
Enabled: true, Enabled: true,
DBFilePath: "leases.db", DBFilePath: "leases.db",
Conf4: dhcpd.V4ServerConf{ Conf4: dhcpd.V4ServerConf{
@ -290,10 +297,9 @@ func TestClientsAddExisting(t *testing.T) {
clients.dhcpServer, err = dhcpd.Create(config) clients.dhcpServer, err = dhcpd.Create(config)
require.NoError(t, err) require.NoError(t, err)
// TODO(e.burkov): leases.db isn't created on Windows so removing it testutil.CleanupAndRequireSuccess(t, func() (err error) {
// causes an error. Split the test to make it run properly on different return os.Remove("leases.db")
// operating systems. })
t.Cleanup(func() { _ = os.Remove("leases.db") })
err = clients.dhcpServer.AddStaticLease(&dhcpd.Lease{ err = clients.dhcpServer.AddStaticLease(&dhcpd.Lease{
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
@ -325,7 +331,7 @@ func TestClientsCustomUpstream(t *testing.T) {
clients := clientsContainer{ clients := clientsContainer{
testing: true, testing: true,
} }
clients.Init(nil, nil, nil) clients.Init(nil, nil, nil, nil)
// Add client with upstreams. // Add client with upstreams.
ok, err := clients.Add(&Client{ ok, err := clients.Add(&Client{

View File

@ -51,6 +51,13 @@ type osConfig struct {
RlimitNoFile uint64 `yaml:"rlimit_nofile"` RlimitNoFile uint64 `yaml:"rlimit_nofile"`
} }
type clientsConfig struct {
// Sources defines the set of sources to fetch the runtime clients from.
Sources *clientSourcesConf `yaml:"runtime_sources"`
// Persistent are the configured clients.
Persistent []*clientObject `yaml:"persistent"`
}
// configuration is loaded from YAML // configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here // field ordering is important -- yaml fields will mirror ordering from here
type configuration struct { type configuration struct {
@ -83,12 +90,12 @@ type configuration struct {
WhitelistFilters []filter `yaml:"whitelist_filters"` WhitelistFilters []filter `yaml:"whitelist_filters"`
UserRules []string `yaml:"user_rules"` UserRules []string `yaml:"user_rules"`
DHCP dhcpd.ServerConfig `yaml:"dhcp"` DHCP *dhcpd.ServerConfig `yaml:"dhcp"`
// Clients contains the YAML representations of the persistent clients. // Clients contains the YAML representations of the persistent clients.
// This field is only used for reading and writing persistent client data. // This field is only used for reading and writing persistent client data.
// Keep this field sorted to ensure consistent ordering. // Keep this field sorted to ensure consistent ordering.
Clients []*clientObject `yaml:"clients"` Clients *clientsConfig `yaml:"clients"`
logSettings `yaml:",inline"` logSettings `yaml:",inline"`
@ -123,13 +130,9 @@ type dnsConfig struct {
// UpstreamTimeout is the timeout for querying upstream servers. // UpstreamTimeout is the timeout for querying upstream servers.
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"` UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
// LocalDomainName is the domain name used for known internal hosts. // PrivateNets is the set of IP networks for which the private reverse DNS
// For example, a machine called "myhost" can be addressed as // resolver should be used.
// "myhost.lan" when LocalDomainName is "lan". PrivateNets []string `yaml:"private_networks"`
LocalDomainName string `yaml:"local_domain_name"`
// ResolveClients enables and disables resolving clients with RDNS.
ResolveClients bool `yaml:"resolve_clients"`
// UsePrivateRDNS defines if the PTR requests for unknown addresses from // UsePrivateRDNS defines if the PTR requests for unknown addresses from
// locally-served networks should be resolved via private PTR resolvers. // locally-served networks should be resolved via private PTR resolvers.
@ -199,8 +202,6 @@ var config = &configuration{
FilteringEnabled: true, // whether or not use filter lists FilteringEnabled: true, // whether or not use filter lists
FiltersUpdateIntervalHours: 24, FiltersUpdateIntervalHours: 24,
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
LocalDomainName: "lan",
ResolveClients: true,
UsePrivateRDNS: true, UsePrivateRDNS: true,
}, },
TLS: tlsConfigSettings{ TLS: tlsConfigSettings{
@ -208,6 +209,18 @@ var config = &configuration{
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
PortDNSOverQUIC: defaultPortQUIC, PortDNSOverQUIC: defaultPortQUIC,
}, },
DHCP: &dhcpd.ServerConfig{
LocalDomainName: "lan",
},
Clients: &clientsConfig{
Sources: &clientSourcesConf{
WHOIS: true,
ARP: true,
RDNS: true,
DHCP: true,
HostsFile: true,
},
},
logSettings: logSettings{ logSettings: logSettings{
LogCompress: false, LogCompress: false,
LogLocalTime: false, LogLocalTime: false,
@ -403,18 +416,16 @@ func (c *configuration) write() error {
s.WriteDiskConfig(&c) s.WriteDiskConfig(&c)
dns := &config.DNS dns := &config.DNS
dns.FilteringConfig = c dns.FilteringConfig = c
dns.LocalPTRResolvers, dns.LocalPTRResolvers, config.Clients.Sources.RDNS, dns.UsePrivateRDNS = s.RDNSSettings()
dns.ResolveClients,
dns.UsePrivateRDNS = s.RDNSSettings()
} }
if Context.dhcpServer != nil { if Context.dhcpServer != nil {
c := dhcpd.ServerConfig{} c := &dhcpd.ServerConfig{}
Context.dhcpServer.WriteDiskConfig(&c) Context.dhcpServer.WriteDiskConfig(c)
config.DHCP = c config.DHCP = c
} }
config.Clients = Context.clients.forConfig() config.Clients.Persistent = Context.clients.forConfig()
configFile := config.getConfigFilename() configFile := config.getConfigFilename()
log.Debug("Writing YAML file: %s", configFile) log.Debug("Writing YAML file: %s", configFile)

View File

@ -13,6 +13,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"time" "time"
"unicode/utf8"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@ -359,6 +360,9 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
} }
} }
// PasswordMinRunes is the minimum length of user's password in runes.
const PasswordMinRunes = 8
// Apply new configuration, start DNS server, restart Web server // Apply new configuration, start DNS server, restart Web server
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
req, restartHTTP, err := decodeApplyConfigReq(r.Body) req, restartHTTP, err := decodeApplyConfigReq(r.Body)
@ -368,6 +372,18 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return return
} }
if utf8.RuneCountInString(req.Password) < PasswordMinRunes {
aghhttp.Error(
r,
w,
http.StatusUnprocessableEntity,
"password must be at least %d symbols long",
PasswordMinRunes,
)
return
}
err = aghnet.CheckPort("udp", req.DNS.IP, req.DNS.Port) err = aghnet.CheckPort("udp", req.DNS.IP, req.DNS.Port)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)

View File

@ -77,13 +77,36 @@ func initDNSServer() (err error) {
filterConf.HTTPRegister = httpRegister filterConf.HTTPRegister = httpRegister
Context.dnsFilter = filtering.New(&filterConf, nil) Context.dnsFilter = filtering.New(&filterConf, nil)
var privateNets netutil.SubnetSet
switch len(config.DNS.PrivateNets) {
case 0:
// Use an optimized locally-served matcher.
privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
case 1:
var n *net.IPNet
n, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
if err != nil {
return fmt.Errorf("preparing the set of private subnets: %w", err)
}
privateNets = n
default:
var nets []*net.IPNet
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
if err != nil {
return fmt.Errorf("preparing the set of private subnets: %w", err)
}
privateNets = netutil.SliceSubnetSet(nets)
}
p := dnsforward.DNSCreateParams{ p := dnsforward.DNSCreateParams{
DNSFilter: Context.dnsFilter, DNSFilter: Context.dnsFilter,
Stats: Context.stats, Stats: Context.stats,
QueryLog: Context.queryLog, QueryLog: Context.queryLog,
SubnetDetector: Context.subnetDetector, PrivateNets: privateNets,
Anonymizer: anonymizer, Anonymizer: anonymizer,
LocalDomain: config.DNS.LocalDomainName, LocalDomain: config.DHCP.LocalDomainName,
} }
if Context.dhcpServer != nil { if Context.dhcpServer != nil {
p.DHCPServer = Context.dhcpServer p.DHCPServer = Context.dhcpServer
@ -112,8 +135,13 @@ func initDNSServer() (err error) {
return fmt.Errorf("dnsServer.Prepare: %w", err) return fmt.Errorf("dnsServer.Prepare: %w", err)
} }
Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS) if config.Clients.Sources.RDNS {
Context.whois = initWHOIS(&Context.clients) Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS)
}
if config.Clients.Sources.WHOIS {
Context.whois = initWHOIS(&Context.clients)
}
Context.filters.Init() Context.filters.Init()
return nil return nil
@ -130,10 +158,11 @@ func onDNSRequest(pctx *proxy.DNSContext) {
return return
} }
if config.DNS.ResolveClients && !ip.IsLoopback() { srcs := config.Clients.Sources
if srcs.RDNS && !ip.IsLoopback() {
Context.rdns.Begin(ip) Context.rdns.Begin(ip)
} }
if !Context.subnetDetector.IsSpecialNetwork(ip) { if srcs.WHOIS && !netutil.IsSpecialPurpose(ip) {
Context.whois.Begin(ip) Context.whois.Begin(ip)
} }
} }
@ -192,6 +221,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
newConf.TLSConfig = tlsConf.TLSConfig newConf.TLSConfig = tlsConf.TLSConfig
newConf.TLSConfig.ServerName = tlsConf.ServerName newConf.TLSConfig.ServerName = tlsConf.ServerName
if tlsConf.PortHTTPS != 0 {
newConf.HTTPSListenAddrs = ipsToTCPAddrs(hosts, tlsConf.PortHTTPS)
}
if tlsConf.PortDNSOverTLS != 0 { if tlsConf.PortDNSOverTLS != 0 {
newConf.TLSListenAddrs = ipsToTCPAddrs(hosts, tlsConf.PortDNSOverTLS) newConf.TLSListenAddrs = ipsToTCPAddrs(hosts, tlsConf.PortDNSOverTLS)
} }
@ -217,7 +250,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
newConf.FilterHandler = applyAdditionalFiltering newConf.FilterHandler = applyAdditionalFiltering
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
newConf.ResolveClients = dnsConf.ResolveClients newConf.ResolveClients = config.Clients.Sources.RDNS
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
@ -365,10 +398,15 @@ func startDNSServer() error {
const topClientsNumber = 100 // the number of clients to get const topClientsNumber = 100 // the number of clients to get
for _, ip := range Context.stats.GetTopClientsIP(topClientsNumber) { for _, ip := range Context.stats.GetTopClientsIP(topClientsNumber) {
if config.DNS.ResolveClients && !ip.IsLoopback() { if ip == nil {
continue
}
srcs := config.Clients.Sources
if srcs.RDNS && !ip.IsLoopback() {
Context.rdns.Begin(ip) Context.rdns.Begin(ip)
} }
if !Context.subnetDetector.IsSpecialNetwork(ip) { if srcs.WHOIS && !netutil.IsSpecialPurpose(ip) {
Context.whois.Begin(ip) Context.whois.Begin(ip)
} }
} }

View File

@ -65,8 +65,6 @@ type homeContext struct {
updater *updater.Updater updater *updater.Updater
subnetDetector *aghnet.SubnetDetector
// mux is our custom http.ServeMux. // mux is our custom http.ServeMux.
mux *http.ServeMux mux *http.ServeMux
@ -175,6 +173,11 @@ func setupContext(args options) {
os.Exit(0) os.Exit(0)
} }
if !args.noEtcHosts && config.Clients.Sources.HostsFile {
err = setupHostsContainer()
fatalOnError(err)
}
} }
Context.mux = http.NewServeMux() Context.mux = http.NewServeMux()
@ -182,7 +185,7 @@ func setupContext(args options) {
// logIfUnsupported logs a formatted warning if the error is one of the // logIfUnsupported logs a formatted warning if the error is one of the
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns // unsupported errors and returns nil. If err is nil, logIfUnsupported returns
// nil. Otherise, it returns err. // nil. Otherwise, it returns err.
func logIfUnsupported(msg string, err error) (outErr error) { func logIfUnsupported(msg string, err error) (outErr error) {
if errors.As(err, new(*aghos.UnsupportedError)) { if errors.As(err, new(*aghos.UnsupportedError)) {
log.Debug(msg, err) log.Debug(msg, err)
@ -287,13 +290,12 @@ func setupConfig(args options) (err error) {
ConfName: config.getConfigFilename(), ConfName: config.getConfigFilename(),
}) })
if !args.noEtcHosts { var arpdb aghnet.ARPDB
if err = setupHostsContainer(); err != nil { if config.Clients.Sources.ARP {
return err arpdb = aghnet.NewARPDB()
}
} }
Context.clients.Init(config.Clients, Context.dhcpServer, Context.etcHosts) Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
if args.bindPort != 0 { if args.bindPort != 0 {
uc := aghalg.UniqChecker{} uc := aghalg.UniqChecker{}
@ -466,9 +468,6 @@ func run(args options, clientBuildFS fs.FS) {
Context.web, err = initWeb(args, clientBuildFS) Context.web, err = initWeb(args, clientBuildFS)
fatalOnError(err) fatalOnError(err)
Context.subnetDetector, err = aghnet.NewSubnetDetector()
fatalOnError(err)
if !Context.firstRun { if !Context.firstRun {
err = initDNSServer() err = initDNSServer()
fatalOnError(err) fatalOnError(err)
@ -527,8 +526,10 @@ func checkPermissions() {
if err != nil { if err != nil {
if errors.Is(err, os.ErrPermission) { if errors.Is(err, os.ErrPermission) {
log.Fatal(`Permission check failed. log.Fatal(`Permission check failed.
AdGuard Home is not allowed to bind to privileged ports (for instance, port 53). AdGuard Home is not allowed to bind to privileged ports (for instance, port 53).
Please note, that this is crucial for a server to be able to use privileged ports. Please note, that this is crucial for a server to be able to use privileged ports.
You have two options: You have two options:
1. Run AdGuard Home with root privileges 1. Run AdGuard Home with root privileges
2. On Linux you can grant the CAP_NET_BIND_SERVICE capability: 2. On Linux you can grant the CAP_NET_BIND_SERVICE capability:

View File

@ -230,13 +230,19 @@ var helpArg = arg{
} }
var noEtcHostsArg = arg{ var noEtcHostsArg = arg{
description: "Do not use the OS-provided hosts.", description: "Deprecated. Do not use the OS-provided hosts.",
longName: "no-etc-hosts", longName: "no-etc-hosts",
shortName: "", shortName: "",
updateWithValue: nil, updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil }, updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
effect: nil, effect: func(_ options, _ string) (f effect, err error) {
serialize: func(o options) []string { return boolSliceOrNil(o.noEtcHosts) }, log.Info(
"warning: --no-etc-hosts flag is deprecated and will be removed in the future versions",
)
return nil, nil
},
serialize: func(o options) []string { return boolSliceOrNil(o.noEtcHosts) },
} }
var localFrontendArg = arg{ var localFrontendArg = arg{

View File

@ -167,7 +167,7 @@ func TestRDNS_WorkerLoop(t *testing.T) {
w := &bytes.Buffer{} w := &bytes.Buffer{}
aghtest.ReplaceLogWriter(t, w) aghtest.ReplaceLogWriter(t, w)
locUpstream := &aghtest.TestUpstream{ locUpstream := &aghtest.Upstream{
Reverse: map[string][]string{ Reverse: map[string][]string{
"192.168.1.1": {"local.domain"}, "192.168.1.1": {"local.domain"},
"2a00:1450:400c:c06::93": {"ipv6.domain"}, "2a00:1450:400c:c06::93": {"ipv6.domain"},

View File

@ -21,9 +21,11 @@ import (
) )
// currentSchemaVersion is the current schema version. // currentSchemaVersion is the current schema version.
const currentSchemaVersion = 12 const currentSchemaVersion = 14
// These aliases are provided for convenience. // These aliases are provided for convenience.
//
// TODO(e.burkov): Remove any after updating to Go 1.18.
type ( type (
any = interface{} any = interface{}
yarr = []any yarr = []any
@ -85,6 +87,8 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
upgradeSchema9to10, upgradeSchema9to10,
upgradeSchema10to11, upgradeSchema10to11,
upgradeSchema11to12, upgradeSchema11to12,
upgradeSchema12to13,
upgradeSchema13to14,
} }
n := 0 n := 0
@ -690,6 +694,114 @@ func upgradeSchema11to12(diskConf yobj) (err error) {
return nil return nil
} }
// upgradeSchema12to13 performs the following changes:
//
// # BEFORE:
// 'dns':
// # …
// 'local_domain_name': 'lan'
//
// # AFTER:
// 'dhcp':
// # …
// 'local_domain_name': 'lan'
//
func upgradeSchema12to13(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 12 to 13")
diskConf["schema_version"] = 13
dnsVal, ok := diskConf["dns"]
if !ok {
return nil
}
var dns yobj
dns, ok = dnsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
}
dhcpVal, ok := diskConf["dhcp"]
if !ok {
return nil
}
var dhcp yobj
dhcp, ok = dhcpVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dhcp: %T", dhcpVal)
}
const field = "local_domain_name"
dhcp[field] = dns[field]
delete(dns, field)
return nil
}
// upgradeSchema13to14 performs the following changes:
//
// # BEFORE:
// 'clients':
// - 'name': 'client-name'
// # …
//
// # AFTER:
// 'clients':
// 'persistent':
// - 'name': 'client-name'
// # …
// 'runtime_sources':
// 'whois': true
// 'arp': true
// 'rdns': true
// 'dhcp': true
// 'hosts': true
//
func upgradeSchema13to14(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 13 to 14")
diskConf["schema_version"] = 14
clientsVal, ok := diskConf["clients"]
if !ok {
clientsVal = yarr{}
}
var rdnsSrc bool
if dnsVal, dok := diskConf["dns"]; dok {
var dnsSettings yobj
dnsSettings, ok = dnsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
}
var rdnsSrcVal any
rdnsSrcVal, ok = dnsSettings["resolve_clients"]
if ok {
rdnsSrc, ok = rdnsSrcVal.(bool)
if !ok {
return fmt.Errorf("unexpected type of resolve_clients: %T", rdnsSrcVal)
}
delete(dnsSettings, "resolve_clients")
}
}
diskConf["clients"] = yobj{
"persistent": clientsVal,
"runtime_sources": &clientSourcesConf{
WHOIS: true,
ARP: true,
RDNS: rdnsSrc,
DHCP: true,
HostsFile: true,
},
}
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

@ -55,7 +55,7 @@ func TestUpgradeSchema2to3(t *testing.T) {
require.Len(t, v, 1) require.Len(t, v, 1)
require.Equal(t, "8.8.8.8:53", v[0]) require.Equal(t, "8.8.8.8:53", v[0])
default: default:
t.Fatalf("wrong type for bootsrap dns: %T", v) t.Fatalf("wrong type for bootstrap dns: %T", v)
} }
excludedEntries := []string{"bootstrap_dns"} excludedEntries := []string{"bootstrap_dns"}
@ -511,3 +511,131 @@ func TestUpgradeSchema11to12(t *testing.T) {
assert.Equal(t, 90*24*time.Hour, ivlVal.Duration) assert.Equal(t, 90*24*time.Hour, ivlVal.Duration)
}) })
} }
func TestUpgradeSchema12to13(t *testing.T) {
const newSchemaVer = 13
testCases := []struct {
in yobj
want yobj
name string
}{{
in: yobj{},
want: yobj{"schema_version": newSchemaVer},
name: "no_dns",
}, {
in: yobj{"dns": yobj{}},
want: yobj{
"dns": yobj{},
"schema_version": newSchemaVer,
},
name: "no_dhcp",
}, {
in: yobj{
"dns": yobj{
"local_domain_name": "lan",
},
"dhcp": yobj{},
"schema_version": newSchemaVer - 1,
},
want: yobj{
"dns": yobj{},
"dhcp": yobj{
"local_domain_name": "lan",
},
"schema_version": newSchemaVer,
},
name: "good",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema12to13(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}
func TestUpgradeSchema13to14(t *testing.T) {
const newSchemaVer = 14
testClient := &clientObject{
Name: "agh-client",
IDs: []string{"id1"},
UseGlobalSettings: true,
}
testCases := []struct {
in yobj
want yobj
name string
}{{
in: yobj{},
want: yobj{
"schema_version": newSchemaVer,
// The clients field will be added anyway.
"clients": yobj{
"persistent": yarr{},
"runtime_sources": &clientSourcesConf{
WHOIS: true,
ARP: true,
RDNS: false,
DHCP: true,
HostsFile: true,
},
},
},
name: "no_clients",
}, {
in: yobj{
"clients": []*clientObject{testClient},
},
want: yobj{
"schema_version": newSchemaVer,
"clients": yobj{
"persistent": []*clientObject{testClient},
"runtime_sources": &clientSourcesConf{
WHOIS: true,
ARP: true,
RDNS: false,
DHCP: true,
HostsFile: true,
},
},
},
name: "no_dns",
}, {
in: yobj{
"clients": []*clientObject{testClient},
"dns": yobj{
"resolve_clients": true,
},
},
want: yobj{
"schema_version": newSchemaVer,
"clients": yobj{
"persistent": []*clientObject{testClient},
"runtime_sources": &clientSourcesConf{
WHOIS: true,
ARP: true,
RDNS: true,
DHCP: true,
HostsFile: true,
},
},
"dns": yobj{},
},
name: "good",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema13to14(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}

View File

@ -34,14 +34,13 @@ const (
) )
type webConfig struct { type webConfig struct {
clientFS fs.FS
clientBetaFS fs.FS
BindHost net.IP BindHost net.IP
BindPort int BindPort int
BetaBindPort int BetaBindPort int
PortHTTPS int PortHTTPS int
firstRun bool
clientFS fs.FS
clientBetaFS fs.FS
// ReadTimeout is an option to pass to http.Server for setting an // ReadTimeout is an option to pass to http.Server for setting an
// appropriate field. // appropriate field.
@ -54,6 +53,8 @@ type webConfig struct {
// WriteTimeout is an option to pass to http.Server for setting an // WriteTimeout is an option to pass to http.Server for setting an
// appropriate field. // appropriate field.
WriteTimeout time.Duration WriteTimeout time.Duration
firstRun bool
} }
// HTTPSServer - HTTPS Server // HTTPSServer - HTTPS Server

View File

@ -4,14 +4,14 @@
## v0.108.0: API changes ## v0.108.0: API changes
## v0.107.7: API changes
### The new possible status code in `/install/configure` response. ### The new possible status code in `/install/configure` response.
* The new status code `422 Unprocessable Entity` in the response for * The new status code `422 Unprocessable Entity` in the response for
`POST /install/configure` which means that the specified password does not `POST /install/configure` which means that the specified password does not
meet the strength requirements. meet the strength requirements.
## v0.107.7: API changes
### The new optional field `"ecs"` in `QueryLogItem` ### The new optional field `"ecs"` in `QueryLogItem`
* The new optional field `"ecs"` in `GET /control/querylog` contains the IP * The new optional field `"ecs"` in `GET /control/querylog` contains the IP

View File

@ -1088,6 +1088,9 @@
'description': > 'description': >
Failed to parse initial configuration or cannot listen to the Failed to parse initial configuration or cannot listen to the
specified addresses. specified addresses.
'422':
'description': >
The specified password does not meet the strength requirements.
'500': '500':
'description': 'Cannot start the DNS server' 'description': 'Cannot start the DNS server'
'/login': '/login':