Pull request 2100: v0.107.42-rc

Squashed commit of the following:

commit 284190f748345c7556e60b67f051ec5f6f080948
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Dec 6 19:36:00 2023 +0300

    all: sync with master; upd chlog
This commit is contained in:
Ainar Garipov
2023-12-07 17:23:00 +03:00
parent df91f016f2
commit 25918e56fa
148 changed files with 7204 additions and 1705 deletions

View File

@@ -0,0 +1,35 @@
package dnssvc
import (
"net/netip"
"time"
)
// Config is the AdGuard Home DNS service configuration structure.
//
// TODO(a.garipov): Add timeout for incoming requests.
type Config struct {
// Addresses are the addresses on which to serve plain DNS queries.
Addresses []netip.AddrPort
// BootstrapServers are the addresses of DNS servers used for bootstrapping
// the upstream DNS server addresses.
BootstrapServers []string
// UpstreamServers are the upstream DNS server addresses to use.
UpstreamServers []string
// DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64. See
// also [Config.UseDNS64].
DNS64Prefixes []netip.Prefix
// UpstreamTimeout is the timeout for upstream requests.
UpstreamTimeout time.Duration
// BootstrapPreferIPv6, if true, instructs the bootstrapper to prefer IPv6
// addresses to IPv4 ones when bootstrapping.
BootstrapPreferIPv6 bool
// UseDNS64, if true, enables DNS64 protection for incoming requests.
UseDNS64 bool
}

View File

@@ -0,0 +1,232 @@
// Package dnssvc contains the AdGuard Home DNS service.
//
// TODO(a.garipov): Define, if all methods of a *Service should work with a nil
// receiver.
package dnssvc
import (
"context"
"fmt"
"net"
"net/netip"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
// and replacement of module dnsproxy.
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
)
// Service is the AdGuard Home DNS service. A nil *Service is a valid
// [agh.Service] that does nothing.
//
// TODO(a.garipov): Consider saving a [*proxy.Config] instance for those
// fields that are only used in [New] and [Service.Config].
type Service struct {
proxy *proxy.Proxy
bootstraps []string
bootstrapResolvers []*upstream.UpstreamResolver
upstreams []string
dns64Prefixes []netip.Prefix
upsTimeout time.Duration
running atomic.Bool
bootstrapPreferIPv6 bool
useDNS64 bool
}
// New returns a new properly initialized *Service. If c is nil, svc is a nil
// *Service that does nothing. The fields of c must not be modified after
// calling New.
func New(c *Config) (svc *Service, err error) {
if c == nil {
return nil, nil
}
svc = &Service{
bootstraps: c.BootstrapServers,
upstreams: c.UpstreamServers,
dns64Prefixes: c.DNS64Prefixes,
upsTimeout: c.UpstreamTimeout,
bootstrapPreferIPv6: c.BootstrapPreferIPv6,
useDNS64: c.UseDNS64,
}
upstreams, resolvers, err := addressesToUpstreams(
c.UpstreamServers,
c.BootstrapServers,
c.UpstreamTimeout,
c.BootstrapPreferIPv6,
)
if err != nil {
return nil, fmt.Errorf("converting upstreams: %w", err)
}
svc.bootstrapResolvers = resolvers
svc.proxy = &proxy.Proxy{
Config: proxy.Config{
UDPListenAddr: udpAddrs(c.Addresses),
TCPListenAddr: tcpAddrs(c.Addresses),
UpstreamConfig: &proxy.UpstreamConfig{
Upstreams: upstreams,
},
UseDNS64: c.UseDNS64,
DNS64Prefs: c.DNS64Prefixes,
},
}
err = svc.proxy.Init()
if err != nil {
return nil, fmt.Errorf("proxy: %w", err)
}
return svc, nil
}
// addressesToUpstreams is a wrapper around [upstream.AddressToUpstream]. It
// accepts a slice of addresses and other upstream parameters, and returns a
// slice of upstreams.
func addressesToUpstreams(
upsStrs []string,
bootstraps []string,
timeout time.Duration,
preferIPv6 bool,
) (upstreams []upstream.Upstream, boots []*upstream.UpstreamResolver, err error) {
opts := &upstream.Options{
Timeout: timeout,
PreferIPv6: preferIPv6,
}
boots, err = aghnet.ParseBootstraps(bootstraps, opts)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return nil, nil, err
}
// TODO(e.burkov): Add system hosts resolver here.
var bootstrap upstream.ParallelResolver
for _, r := range boots {
bootstrap = append(bootstrap, r)
}
upstreams = make([]upstream.Upstream, len(upsStrs))
for i, upsStr := range upsStrs {
upstreams[i], err = upstream.AddressToUpstream(upsStr, &upstream.Options{
Bootstrap: bootstrap,
Timeout: timeout,
PreferIPv6: preferIPv6,
})
if err != nil {
return nil, boots, fmt.Errorf("upstream at index %d: %w", i, err)
}
}
return upstreams, boots, nil
}
// tcpAddrs converts []netip.AddrPort into []*net.TCPAddr.
func tcpAddrs(addrPorts []netip.AddrPort) (tcpAddrs []*net.TCPAddr) {
if addrPorts == nil {
return nil
}
tcpAddrs = make([]*net.TCPAddr, len(addrPorts))
for i, a := range addrPorts {
tcpAddrs[i] = net.TCPAddrFromAddrPort(a)
}
return tcpAddrs
}
// udpAddrs converts []netip.AddrPort into []*net.UDPAddr.
func udpAddrs(addrPorts []netip.AddrPort) (udpAddrs []*net.UDPAddr) {
if addrPorts == nil {
return nil
}
udpAddrs = make([]*net.UDPAddr, len(addrPorts))
for i, a := range addrPorts {
udpAddrs[i] = net.UDPAddrFromAddrPort(a)
}
return udpAddrs
}
// type check
var _ agh.Service = (*Service)(nil)
// Start implements the [agh.Service] interface for *Service. svc may be nil.
// After Start exits, all DNS servers have tried to start, but there is no
// guarantee that they did. Errors from the servers are written to the log.
func (svc *Service) Start() (err error) {
if svc == nil {
return nil
}
defer func() {
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
// tell when all servers are actually up, so at best this is merely an
// assumption.
svc.running.Store(err == nil)
}()
return svc.proxy.Start()
}
// Shutdown implements the [agh.Service] interface for *Service. svc may be
// nil.
func (svc *Service) Shutdown(ctx context.Context) (err error) {
if svc == nil {
return nil
}
errs := []error{
svc.proxy.Stop(),
}
for _, b := range svc.bootstrapResolvers {
errs = append(errs, errors.Annotate(b.Close(), "closing bootstrap %s: %w", b.Address()))
}
return errors.Join(errs...)
}
// Config returns the current configuration of the web service. Config must not
// be called simultaneously with Start. If svc was initialized with ":0"
// addresses, addrs will not return the actual bound ports until Start is
// finished.
func (svc *Service) Config() (c *Config) {
// TODO(a.garipov): Do we need to get the TCP addresses separately?
var addrs []netip.AddrPort
if svc.running.Load() {
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
addrs = make([]netip.AddrPort, len(udpAddrs))
for i, a := range udpAddrs {
addrs[i] = a.(*net.UDPAddr).AddrPort()
}
} else {
conf := svc.proxy.Config
udpAddrs := conf.UDPListenAddr
addrs = make([]netip.AddrPort, len(udpAddrs))
for i, a := range udpAddrs {
addrs[i] = a.AddrPort()
}
}
c = &Config{
Addresses: addrs,
BootstrapServers: svc.bootstraps,
UpstreamServers: svc.upstreams,
DNS64Prefixes: svc.dns64Prefixes,
UpstreamTimeout: svc.upsTimeout,
BootstrapPreferIPv6: svc.bootstrapPreferIPv6,
UseDNS64: svc.useDNS64,
}
return c
}

View File

@@ -0,0 +1,125 @@
package dnssvc_test
import (
"context"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second
func TestService(t *testing.T) {
const (
listenAddr = "127.0.0.1:0"
bootstrapAddr = "127.0.0.1:0"
upstreamAddr = "upstream.example"
)
upstreamErrCh := make(chan error, 1)
upstreamStartedCh := make(chan struct{})
upstreamSrv := &dns.Server{
Addr: bootstrapAddr,
Net: "udp",
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
pt := testutil.PanicT{}
resp := (&dns.Msg{}).SetReply(req)
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{},
A: netip.MustParseAddrPort(bootstrapAddr).Addr().AsSlice(),
})
writeErr := w.WriteMsg(resp)
require.NoError(pt, writeErr)
}),
NotifyStartedFunc: func() { close(upstreamStartedCh) },
}
go func() {
listenErr := upstreamSrv.ListenAndServe()
if listenErr != nil {
// Log these immediately to see what happens.
t.Logf("upstream listen error: %s", listenErr)
}
upstreamErrCh <- listenErr
}()
_, _ = testutil.RequireReceive(t, upstreamStartedCh, testTimeout)
c := &dnssvc.Config{
Addresses: []netip.AddrPort{netip.MustParseAddrPort(listenAddr)},
BootstrapServers: []string{upstreamSrv.PacketConn.LocalAddr().String()},
UpstreamServers: []string{upstreamAddr},
DNS64Prefixes: nil,
UpstreamTimeout: testTimeout,
BootstrapPreferIPv6: false,
UseDNS64: false,
}
svc, err := dnssvc.New(c)
require.NoError(t, err)
err = svc.Start()
require.NoError(t, err)
gotConf := svc.Config()
require.NotNil(t, gotConf)
require.Len(t, gotConf.Addresses, 1)
addr := gotConf.Addresses[0]
t.Run("dns", func(t *testing.T) {
req := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{{
Name: "example.com.",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}},
}
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
cli := &dns.Client{}
var resp *dns.Msg
require.Eventually(t, func() (ok bool) {
var excErr error
resp, _, excErr = cli.ExchangeContext(ctx, req, addr.String())
return excErr == nil
}, testTimeout, testTimeout/10)
assert.NotNil(t, resp)
})
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
err = svc.Shutdown(ctx)
require.NoError(t, err)
err = upstreamSrv.Shutdown()
require.NoError(t, err)
err, ok := testutil.RequireReceive(t, upstreamErrCh, testTimeout)
require.True(t, ok)
require.NoError(t, err)
}