Merge branch 'master' into 7555-fix-ra-packets

This commit is contained in:
Eugene Burkov
2025-03-06 16:03:25 +03:00
187 changed files with 8839 additions and 11076 deletions

View File

@@ -0,0 +1,24 @@
package aghnet
import "github.com/AdguardTeam/dnsproxy/upstream"
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
if !http3 {
return upstream.DefaultHTTPVersions
}
return []upstream.HTTPVersion{
upstream.HTTPVersion3,
upstream.HTTPVersion2,
upstream.HTTPVersion11,
}
}
// IsCommentOrEmpty returns true if s starts with a "#" character or is empty.
// This function is useful for filtering out non-upstream lines from upstream
// configs.
func IsCommentOrEmpty(s string) (ok bool) {
return len(s) == 0 || s[0] == '#'
}

View File

@@ -0,0 +1,26 @@
package aghnet_test
import (
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/stretchr/testify/assert"
)
func TestIsCommentOrEmpty(t *testing.T) {
for _, tc := range []struct {
want assert.BoolAssertionFunc
str string
}{{
want: assert.True,
str: "",
}, {
want: assert.True,
str: "# comment",
}, {
want: assert.False,
str: "1.2.3.4",
}} {
tc.want(t, aghnet.IsCommentOrEmpty(tc.str))
}
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns"
)
@@ -121,26 +120,6 @@ func (p *AddressUpdater) UpdateAddress(
p.OnUpdateAddress(ctx, ip, host, info)
}
// Package dnsforward
// ClientsContainer is a fake [dnsforward.ClientsContainer] implementation for
// tests.
type ClientsContainer struct {
OnUpstreamConfigByID func(
id string,
boot upstream.Resolver,
) (conf *proxy.CustomUpstreamConfig, err error)
}
// UpstreamConfigByID implements the [dnsforward.ClientsContainer] interface
// for *ClientsContainer.
func (c *ClientsContainer) UpstreamConfigByID(
id string,
boot upstream.Resolver,
) (conf *proxy.CustomUpstreamConfig, err error) {
return c.OnUpstreamConfigByID(id, boot)
}
// Package filtering
// Resolver is a fake [filtering.Resolver] implementation for tests.

View File

@@ -3,7 +3,6 @@ package aghtest_test
import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
)
@@ -12,9 +11,6 @@ import (
// type check
var _ filtering.Resolver = (*aghtest.Resolver)(nil)
// type check
var _ dnsforward.ClientsContainer = (*aghtest.ClientsContainer)(nil)
// type check
//
// TODO(s.chzhen): It's here to avoid the import cycle. Remove it.

View File

@@ -178,8 +178,12 @@ func (r *Runtime) Addr() (ip netip.Addr) {
return r.ip
}
// clone returns a deep copy of the runtime client.
// clone returns a deep copy of the runtime client. If r is nil, c is nil.
func (r *Runtime) clone() (c *Runtime) {
if r == nil {
return nil
}
return &Runtime{
ip: r.ip,
whois: r.whois.Clone(),

View File

@@ -9,7 +9,6 @@ import (
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/golibs/errors"
)
// macKey contains MAC as byte array of 6, 8, or 20 bytes.
@@ -35,7 +34,7 @@ type index struct {
// nameToUID maps client name to UID.
nameToUID map[string]UID
// clientIDToUID maps client ID to UID.
// clientIDToUID maps ClientID to UID.
clientIDToUID map[string]UID
// ipToUID maps IP address to UID.
@@ -205,19 +204,19 @@ func (ci *index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr)
return nil, nil
}
// find finds persistent client by string representation of the client ID, IP
// find finds persistent client by string representation of the ClientID, IP
// address, or MAC.
func (ci *index) find(id string) (c *Persistent, ok bool) {
uid, found := ci.clientIDToUID[id]
if found {
return ci.uidToClient[uid], true
c, ok = ci.findByClientID(id)
if ok {
return c, true
}
ip, err := netip.ParseAddr(id)
if err == nil {
// MAC addresses can be successfully parsed as IP addresses.
c, found = ci.findByIP(ip)
if found {
c, ok = ci.findByIP(ip)
if ok {
return c, true
}
}
@@ -230,6 +229,16 @@ func (ci *index) find(id string) (c *Persistent, ok bool) {
return nil, false
}
// findByClientID finds persistent client by ClientID.
func (ci *index) findByClientID(clientID string) (c *Persistent, ok bool) {
uid, ok := ci.clientIDToUID[clientID]
if ok {
return ci.uidToClient[uid], true
}
return nil, false
}
// findByName finds persistent client by name.
func (ci *index) findByName(name string) (c *Persistent, found bool) {
uid, found := ci.nameToUID[name]
@@ -343,18 +352,3 @@ func (ci *index) rangeByName(f func(c *Persistent) (cont bool)) {
}
}
}
// closeUpstreams closes upstream configurations of persistent clients.
func (ci *index) closeUpstreams() (err error) {
var errs []error
ci.rangeByName(func(c *Persistent) (cont bool) {
err = c.CloseUpstreams()
if err != nil {
errs = append(errs, err)
}
return true
})
return errors.Join(errs...)
}

View File

@@ -58,12 +58,6 @@ func (uid *UID) UnmarshalText(data []byte) error {
// Persistent contains information about persistent clients.
type Persistent struct {
// UpstreamConfig is the custom upstream configuration for this client. If
// it's nil, it has not been initialized yet. If it's non-nil and empty,
// there are no valid upstreams. If it's non-nil and non-empty, these
// upstream must be used.
UpstreamConfig *proxy.CustomUpstreamConfig
// SafeSearch handles search engine hosts rewrites.
SafeSearch filtering.SafeSearch
@@ -262,7 +256,7 @@ func ValidateClientID(id string) (err error) {
return nil
}
// IDs returns a list of client IDs containing at least one element.
// IDs returns a list of ClientIDs containing at least one element.
func (c *Persistent) IDs() (ids []string) {
ids = make([]string, 0, c.IDsLen())
@@ -281,7 +275,7 @@ func (c *Persistent) IDs() (ids []string) {
return append(ids, c.ClientIDs...)
}
// IDsLen returns a length of client ids.
// IDsLen returns a length of ClientIDs.
func (c *Persistent) IDsLen() (n int) {
return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)
}
@@ -312,14 +306,3 @@ func (c *Persistent) ShallowClone() (clone *Persistent) {
return clone
}
// CloseUpstreams closes the client-specific upstream config of c if any.
func (c *Persistent) CloseUpstreams() (err error) {
if c.UpstreamConfig != nil {
if err = c.UpstreamConfig.Close(); err != nil {
return fmt.Errorf("closing upstreams of client %q: %w", c.Name, err)
}
}
return nil
}

View File

@@ -13,9 +13,11 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
)
// allowedTags is the list of available client tags.
@@ -88,6 +90,10 @@ type StorageConfig struct {
// not be nil.
Logger *slog.Logger
// Clock is used by [upstreamManager] to retrieve the current time. It must
// not be nil.
Clock timeutil.Clock
// DHCP is used to match IPs against MACs of persistent clients and update
// [SourceDHCP] runtime client information. It must not be nil.
DHCP DHCP
@@ -126,6 +132,9 @@ type Storage struct {
// runtimeIndex contains information about runtime clients.
runtimeIndex *runtimeIndex
// upstreamManager stores and updates custom client upstream configurations.
upstreamManager *upstreamManager
// dhcp is used to update [SourceDHCP] runtime client information.
dhcp DHCP
@@ -163,6 +172,7 @@ func NewStorage(ctx context.Context, conf *StorageConfig) (s *Storage, err error
mu: &sync.Mutex{},
index: newIndex(),
runtimeIndex: newRuntimeIndex(),
upstreamManager: newUpstreamManager(conf.Logger, conf.Clock),
dhcp: conf.DHCP,
etcHosts: conf.EtcHosts,
arpDB: conf.ARPDB,
@@ -200,7 +210,7 @@ func (s *Storage) Start(ctx context.Context) (err error) {
func (s *Storage) Shutdown(_ context.Context) (err error) {
close(s.done)
return s.closeUpstreams()
return s.upstreamManager.close()
}
// periodicARPUpdate periodically reloads runtime clients from ARP. It is
@@ -416,6 +426,7 @@ func (s *Storage) Add(ctx context.Context, p *Persistent) (err error) {
}
s.index.add(p)
s.upstreamManager.updateCustomUpstreamConfig(p)
s.logger.DebugContext(
ctx,
@@ -441,7 +452,7 @@ func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
return nil, false
}
// Find finds persistent client by string representation of the client ID, IP
// Find finds persistent client by string representation of the ClientID, IP
// address, or MAC. And returns its shallow copy.
//
// TODO(s.chzhen): Accept ClientIDData structure instead, which will contain
@@ -514,12 +525,13 @@ func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {
return false
}
if err := p.CloseUpstreams(); err != nil {
s.logger.ErrorContext(ctx, "removing client", "name", p.Name, slogutil.KeyError, err)
}
s.index.remove(p)
err := s.upstreamManager.remove(p.UID)
if err != nil {
s.logger.DebugContext(ctx, "closing client upstreams", "name", name, slogutil.KeyError, err)
}
return true
}
@@ -556,6 +568,8 @@ func (s *Storage) Update(ctx context.Context, name string, p *Persistent) (err e
s.index.remove(stored)
s.index.add(p)
s.upstreamManager.updateCustomUpstreamConfig(p)
return nil
}
@@ -576,14 +590,6 @@ func (s *Storage) Size() (n int) {
return s.index.size()
}
// closeUpstreams closes upstream configurations of persistent clients.
func (s *Storage) closeUpstreams() (err error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.index.closeUpstreams()
}
// ClientRuntime returns a copy of the saved runtime client by ip. If no such
// client exists, returns nil.
func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {
@@ -591,17 +597,21 @@ func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {
defer s.mu.Unlock()
rc = s.runtimeIndex.client(ip)
if rc != nil {
if !s.runtimeSourceDHCP {
return rc.clone()
}
if !s.runtimeSourceDHCP {
return nil
// SourceHostsFile > SourceDHCP, so return immediately if the client is from
// the hosts file.
if rc != nil && rc.hostsFile != nil {
return rc.clone()
}
// Otherwise, check the DHCP server and add the client information if there
// is any.
host := s.dhcp.HostByIP(ip)
if host == "" {
return nil
return rc.clone()
}
rc = s.runtimeIndex.setInfo(ip, SourceDHCP, []string{host})
@@ -622,3 +632,42 @@ func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
func (s *Storage) AllowedTags() (tags []string) {
return s.allowedTags
}
// CustomUpstreamConfig implements the [dnsforward.ClientsContainer] interface
// for *Storage
func (s *Storage) CustomUpstreamConfig(
id string,
addr netip.Addr,
) (prxConf *proxy.CustomUpstreamConfig) {
s.mu.Lock()
defer s.mu.Unlock()
c, ok := s.index.findByClientID(id)
if !ok {
c, ok = s.index.findByIP(addr)
}
if !ok {
return nil
}
return s.upstreamManager.customUpstreamConfig(c.UID)
}
// UpdateCommonUpstreamConfig implements the [dnsforward.ClientsContainer]
// interface for *Storage
func (s *Storage) UpdateCommonUpstreamConfig(conf *CommonUpstreamConfig) {
s.mu.Lock()
defer s.mu.Unlock()
s.upstreamManager.updateCommonUpstreamConfig(conf)
}
// ClearUpstreamCache implements the [dnsforward.ClientsContainer] interface for
// *Storage
func (s *Storage) ClearUpstreamCache() {
s.mu.Lock()
defer s.mu.Unlock()
s.upstreamManager.clearUpstreamCache()
}

View File

@@ -13,27 +13,34 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/faketime"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newTestStorage is a helper function that returns initialized storage.
func newTestStorage(tb testing.TB) (s *client.Storage) {
func newTestStorage(tb testing.TB, clock timeutil.Clock) (s *client.Storage) {
tb.Helper()
ctx := testutil.ContextWithTimeout(tb, testTimeout)
s, err := client.NewStorage(ctx, &client.StorageConfig{
Logger: slogutil.NewDiscardLogger(),
Clock: clock,
})
require.NoError(tb, err)
return s
}
// type check
var _ dnsforward.ClientsContainer = (*client.Storage)(nil)
// testHostsContainer is a mock implementation of the [client.HostsContainer]
// interface.
type testHostsContainer struct {
@@ -353,6 +360,9 @@ func TestClientsDHCP(t *testing.T) {
prsCliIP = netip.MustParseAddr("4.3.2.1")
prsCliMAC = mustParseMAC("AA:AA:AA:AA:AA:AA")
prsCliName = "persistent.dhcp"
otherARPCliName = "other.arp"
otherARPCliIP = netip.MustParseAddr("192.0.2.1")
)
ipToHost := map[netip.Addr]string{
@@ -372,7 +382,20 @@ func TestClientsDHCP(t *testing.T) {
HWAddr: cliMAC3,
}}
d := &testDHCP{
arpCh := make(chan []arpdb.Neighbor, 1)
arpDB := &testARPDB{
onRefresh: func() (err error) { return nil },
onNeighbors: func() (ns []arpdb.Neighbor) {
select {
case ns = <-arpCh:
return ns
default:
return nil
}
},
}
dhcp := &testDHCP{
OnLeases: func() (ls []*dhcpsvc.Lease) {
return leases
},
@@ -384,22 +407,111 @@ func TestClientsDHCP(t *testing.T) {
},
}
etcHostsCh := make(chan *hostsfile.DefaultStorage, 1)
etcHosts := &testHostsContainer{
onUpd: func() (updates <-chan *hostsfile.DefaultStorage) {
return etcHostsCh
},
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
storage, err := client.NewStorage(ctx, &client.StorageConfig{
Logger: slogutil.NewDiscardLogger(),
DHCP: d,
RuntimeSourceDHCP: true,
Logger: slogutil.NewDiscardLogger(),
ARPDB: arpDB,
DHCP: dhcp,
EtcHosts: etcHosts,
RuntimeSourceDHCP: true,
ARPClientsUpdatePeriod: testTimeout / 10,
})
require.NoError(t, err)
t.Run("find_runtime", func(t *testing.T) {
err = storage.Start(testutil.ContextWithTimeout(t, testTimeout))
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return storage.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
require.True(t, t.Run("find_runtime_lower_priority", func(t *testing.T) {
// Add a lower-priority client.
ns := []arpdb.Neighbor{{
Name: cliName1,
IP: cliIP1,
}}
testutil.RequireSend(t, arpCh, ns, testTimeout)
storage.ReloadARP(testutil.ContextWithTimeout(t, testTimeout))
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
assert.True(t, compareRuntimeInfo(cli1, client.SourceDHCP, cliName1))
})
t.Run("find_persistent", func(t *testing.T) {
// Remove the matching client.
//
// TODO(a.garipov): Consider adding ways of explicitly clearing runtime
// sources by source.
ns = []arpdb.Neighbor{{
Name: otherARPCliName,
IP: otherARPCliIP,
}}
testutil.RequireSend(t, arpCh, ns, testTimeout)
storage.ReloadARP(testutil.ContextWithTimeout(t, testTimeout))
}))
require.True(t, t.Run("find_runtime", func(t *testing.T) {
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
assert.True(t, compareRuntimeInfo(cli1, client.SourceDHCP, cliName1))
}))
require.True(t, t.Run("find_runtime_higher_priority", func(t *testing.T) {
// Add a higher-priority client.
s, strgErr := hostsfile.NewDefaultStorage()
require.NoError(t, strgErr)
s.Add(&hostsfile.Record{
Addr: cliIP1,
Names: []string{cliName1},
})
testutil.RequireSend(t, etcHostsCh, s, testTimeout)
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
require.Eventually(t, func() (ok bool) {
cli := storage.ClientRuntime(cliIP1)
if cli == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli, client.SourceHostsFile, cliName1))
return true
}, testTimeout, testTimeout/10)
// Remove the matching client.
//
// TODO(a.garipov): Consider adding ways of explicitly clearing runtime
// sources by source.
s, strgErr = hostsfile.NewDefaultStorage()
require.NoError(t, strgErr)
testutil.RequireSend(t, etcHostsCh, s, testTimeout)
require.Eventually(t, func() (ok bool) {
cli := storage.ClientRuntime(cliIP1)
return compareRuntimeInfo(cli, client.SourceDHCP, cliName1)
}, testTimeout, testTimeout/10)
}))
require.True(t, t.Run("find_persistent", func(t *testing.T) {
err = storage.Add(ctx, &client.Persistent{
Name: prsCliName,
UID: client.MustNewUID(),
@@ -411,9 +523,9 @@ func TestClientsDHCP(t *testing.T) {
require.True(t, ok)
assert.Equal(t, prsCliName, prsCli.Name)
})
}))
t.Run("leases", func(t *testing.T) {
require.True(t, t.Run("leases", func(t *testing.T) {
delete(ipToHost, cliIP1)
storage.UpdateDHCP(ctx)
@@ -428,18 +540,20 @@ func TestClientsDHCP(t *testing.T) {
assert.Equal(t, client.SourceDHCP, src)
assert.Equal(t, leases[i].Hostname, host)
}
})
}))
t.Run("range", func(t *testing.T) {
require.True(t, t.Run("range", func(t *testing.T) {
s := 0
storage.RangeRuntime(func(rc *client.Runtime) (cont bool) {
s++
if src, _ := rc.Info(); src == client.SourceDHCP {
s++
}
return true
})
assert.Equal(t, len(leases), s)
})
}))
}
func TestClientsAddExisting(t *testing.T) {
@@ -584,7 +698,7 @@ func TestStorage_Add(t *testing.T) {
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
s := newTestStorage(t)
s := newTestStorage(t, timeutil.SystemClock{})
tags := s.AllowedTags()
require.NotZero(t, len(tags))
require.True(t, slices.IsSorted(tags))
@@ -715,7 +829,7 @@ func TestStorage_RemoveByName(t *testing.T) {
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
s := newTestStorage(t)
s := newTestStorage(t, timeutil.SystemClock{})
err := s.Add(ctx, existingClient)
require.NoError(t, err)
@@ -740,7 +854,7 @@ func TestStorage_RemoveByName(t *testing.T) {
}
t.Run("duplicate_remove", func(t *testing.T) {
s = newTestStorage(t)
s = newTestStorage(t, timeutil.SystemClock{})
err = s.Add(ctx, existingClient)
require.NoError(t, err)
@@ -1171,3 +1285,99 @@ func TestStorage_RangeByName(t *testing.T) {
})
}
}
func TestStorage_CustomUpstreamConfig(t *testing.T) {
const (
existingName = "existing_name"
existingClientID = "existing_client_id"
nonExistingClientID = "non_existing_client_id"
)
var (
existingClientUID = client.MustNewUID()
existingIP = netip.MustParseAddr("192.0.2.1")
nonExistingIP = netip.MustParseAddr("192.0.2.255")
testUpstreamTimeout = time.Second
)
existingClient := &client.Persistent{
Name: existingName,
IPs: []netip.Addr{existingIP},
ClientIDs: []string{existingClientID},
UID: existingClientUID,
Upstreams: []string{"192.0.2.0"},
}
date := time.Now()
clock := &faketime.Clock{
OnNow: func() (now time.Time) {
date = date.Add(time.Second)
return date
},
}
s := newTestStorage(t, clock)
s.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{
UpstreamTimeout: testUpstreamTimeout,
})
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := s.Add(ctx, existingClient)
require.NoError(t, err)
testCases := []struct {
cliAddr netip.Addr
wantNilConf assert.ValueAssertionFunc
name string
cliID string
}{{
name: "client_id",
cliID: existingClientID,
cliAddr: netip.Addr{},
wantNilConf: assert.NotNil,
}, {
name: "client_addr",
cliID: "",
cliAddr: existingIP,
wantNilConf: assert.NotNil,
}, {
name: "non_existing_client_id",
cliID: nonExistingClientID,
cliAddr: netip.Addr{},
wantNilConf: assert.Nil,
}, {
name: "non_existing_client_addr",
cliID: "",
cliAddr: nonExistingIP,
wantNilConf: assert.Nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf := s.CustomUpstreamConfig(tc.cliID, tc.cliAddr)
tc.wantNilConf(t, conf)
})
}
t.Run("update_common_config", func(t *testing.T) {
conf := s.CustomUpstreamConfig(existingClientID, existingIP)
require.NotNil(t, conf)
s.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{
UpstreamTimeout: testUpstreamTimeout * 2,
})
updConf := s.CustomUpstreamConfig(existingClientID, existingIP)
require.NotNil(t, updConf)
assert.NotEqual(t, conf, updConf)
})
}

View File

@@ -0,0 +1,224 @@
package client
import (
"fmt"
"log/slog"
"slices"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil"
)
// CommonUpstreamConfig contains common settings for custom client upstream
// configurations.
type CommonUpstreamConfig struct {
Bootstrap upstream.Resolver
UpstreamTimeout time.Duration
BootstrapPreferIPv6 bool
EDNSClientSubnetEnabled bool
UseHTTP3Upstreams bool
}
// customUpstreamConfig contains custom client upstream configuration and the
// timestamp of the latest configuration update.
type customUpstreamConfig struct {
// proxyConf is the constructed upstream configuration for the [proxy],
// derived from the fields below. It is initialized on demand with
// [newCustomUpstreamConfig].
proxyConf *proxy.CustomUpstreamConfig
// commonConfUpdate is the timestamp of the latest configuration update,
// used to check against [upstreamManager.confUpdate] to determine if the
// configuration is up to date.
commonConfUpdate time.Time
// upstreams is the cached list of custom upstream DNS servers used for the
// configuration of proxyConf.
upstreams []string
// upstreamsCacheSize is the cached value of the cache size of the
// upstreams, used for the configuration of proxyConf.
upstreamsCacheSize uint32
// upstreamsCacheEnabled is the cached value indicating whether the cache of
// the upstreams is enabled for the configuration of proxyConf.
upstreamsCacheEnabled bool
// isChanged indicates whether the proxyConf needs to be updated.
isChanged bool
}
// upstreamManager stores and updates custom client upstream configurations.
type upstreamManager struct {
// logger is used for logging the operation of the upstream manager. It
// must not be nil.
//
// TODO(s.chzhen): Consider using a logger with its own prefix.
logger *slog.Logger
// uidToCustomConf maps persistent client UID to the custom client upstream
// configuration. Stored UIDs must be in sync with the [index.uidToClient].
uidToCustomConf map[UID]*customUpstreamConfig
// commonConf is the common upstream configuration.
commonConf *CommonUpstreamConfig
// clock is used to get the current time. It must not be nil.
clock timeutil.Clock
// confUpdate is the timestamp of the latest common upstream configuration
// update.
confUpdate time.Time
}
// newUpstreamManager returns the new properly initialized upstream manager.
func newUpstreamManager(logger *slog.Logger, clock timeutil.Clock) (m *upstreamManager) {
return &upstreamManager{
logger: logger,
uidToCustomConf: make(map[UID]*customUpstreamConfig),
clock: clock,
}
}
// updateCommonUpstreamConfig updates the common upstream configuration and the
// timestamp of the latest configuration update.
func (m *upstreamManager) updateCommonUpstreamConfig(conf *CommonUpstreamConfig) {
m.commonConf = conf
m.confUpdate = m.clock.Now()
}
// updateCustomUpstreamConfig updates the stored custom client upstream
// configuration associated with the persistent client. It also sets
// [customUpstreamConfig.isChanged] to true so [customUpstreamConfig.proxyConf]
// can be updated later in [upstreamManager.customUpstreamConfig].
func (m *upstreamManager) updateCustomUpstreamConfig(c *Persistent) {
cliConf, ok := m.uidToCustomConf[c.UID]
if !ok {
cliConf = &customUpstreamConfig{
commonConfUpdate: m.confUpdate,
}
m.uidToCustomConf[c.UID] = cliConf
}
// TODO(s.chzhen): Compare before cloning.
cliConf.upstreams = slices.Clone(c.Upstreams)
cliConf.upstreamsCacheSize = c.UpstreamsCacheSize
cliConf.upstreamsCacheEnabled = c.UpstreamsCacheEnabled
cliConf.isChanged = true
}
// customUpstreamConfig returns the custom client upstream configuration.
func (m *upstreamManager) customUpstreamConfig(uid UID) (proxyConf *proxy.CustomUpstreamConfig) {
cliConf, ok := m.uidToCustomConf[uid]
if !ok {
// TODO(s.chzhen): Consider panic.
m.logger.Error("no associated custom client upstream config")
return nil
}
if !m.isConfigChanged(cliConf) {
return cliConf.proxyConf
}
if cliConf.proxyConf != nil {
err := cliConf.proxyConf.Close()
if err != nil {
// TODO(s.chzhen): Pass context.
m.logger.Debug("closing custom upstream config", slogutil.KeyError, err)
}
}
proxyConf = newCustomUpstreamConfig(cliConf, m.commonConf)
cliConf.proxyConf = proxyConf
cliConf.isChanged = false
return proxyConf
}
// isConfigChanged returns true if the update is necessary for the custom client
// upstream configuration.
func (m *upstreamManager) isConfigChanged(cliConf *customUpstreamConfig) (ok bool) {
return !m.confUpdate.Equal(cliConf.commonConfUpdate) || cliConf.isChanged
}
// clearUpstreamCache clears the upstream cache for each stored custom client
// upstream configuration.
func (m *upstreamManager) clearUpstreamCache() {
for _, c := range m.uidToCustomConf {
c.proxyConf.ClearCache()
}
}
// remove deletes the custom client upstream configuration and closes
// [customUpstreamConfig.proxyConf] if necessary.
func (m *upstreamManager) remove(uid UID) (err error) {
cliConf, ok := m.uidToCustomConf[uid]
if !ok {
// TODO(s.chzhen): Consider panic.
return errors.Error("no associated custom client upstream config")
}
delete(m.uidToCustomConf, uid)
if cliConf.proxyConf != nil {
return cliConf.proxyConf.Close()
}
return nil
}
// close shuts down each stored custom client upstream configuration.
func (m *upstreamManager) close() (err error) {
var errs []error
for _, c := range m.uidToCustomConf {
if c.proxyConf == nil {
continue
}
errs = append(errs, c.proxyConf.Close())
}
return errors.Join(errs...)
}
// newCustomUpstreamConfig returns the new properly initialized custom proxy
// upstream configuration for the client.
func newCustomUpstreamConfig(
cliConf *customUpstreamConfig,
conf *CommonUpstreamConfig,
) (proxyConf *proxy.CustomUpstreamConfig) {
upstreams := stringutil.FilterOut(cliConf.upstreams, aghnet.IsCommentOrEmpty)
if len(upstreams) == 0 {
return nil
}
upsConf, err := proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: conf.Bootstrap,
Timeout: time.Duration(conf.UpstreamTimeout),
HTTPVersions: aghnet.UpstreamHTTPVersions(conf.UseHTTP3Upstreams),
PreferIPv6: conf.BootstrapPreferIPv6,
},
)
if err != nil {
// Should not happen because upstreams are already validated. See
// [Persistent.validate].
panic(fmt.Errorf("creating custom upstream config: %w", err))
}
return proxy.NewCustomUpstreamConfig(
upsConf,
cliConf.upstreamsCacheEnabled,
int(cliConf.upstreamsCacheSize),
conf.EDNSClientSubnetEnabled,
)
}

View File

@@ -15,7 +15,7 @@ import (
var _ proxy.BeforeRequestHandler = (*Server)(nil)
// HandleBefore is the handler that is called before any other processing,
// including logs. It performs access checks and puts the client ID, if there
// including logs. It performs access checks and puts the ClientID, if there
// is one, into the server's cache.
//
// TODO(d.kolyshev): Extract to separate package.

View File

@@ -266,6 +266,7 @@ func TestServer_HandleBefore_udp(t *testing.T) {
UpstreamDNS: []string{localUpsAddr},
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})

View File

@@ -62,7 +62,7 @@ func clientIDFromClientServerName(
return strings.ToLower(clientID), nil
}
// clientIDFromDNSContextHTTPS extracts the client's ID from the path of the
// clientIDFromDNSContextHTTPS extracts the ClientID from the path of the
// client's DNS-over-HTTPS request.
func clientIDFromDNSContextHTTPS(pctx *proxy.DNSContext) (clientID string, err error) {
r := pctx.HTTPRequest

View File

@@ -0,0 +1,46 @@
package dnsforward
import (
"net/netip"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/dnsproxy/proxy"
)
// ClientsContainer provides information about preconfigured DNS clients.
type ClientsContainer interface {
// CustomUpstreamConfig returns the custom client upstream configuration, if
// any. It prioritizes ClientID over client IP address to identify the
// client.
CustomUpstreamConfig(clientID string, cliAddr netip.Addr) (conf *proxy.CustomUpstreamConfig)
// UpdateCommonUpstreamConfig updates the common upstream configuration.
UpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig)
// ClearUpstreamCache clears the upstream cache for each stored custom
// client upstream configuration.
ClearUpstreamCache()
}
// EmptyClientsContainer is an [ClientsContainer] implementation that does nothing.
type EmptyClientsContainer struct{}
// type check
var _ ClientsContainer = EmptyClientsContainer{}
// CustomUpstreamConfig implements the [ClientsContainer] interface for
// EmptyClientsContainer.
func (EmptyClientsContainer) CustomUpstreamConfig(
clientID string,
cliAddr netip.Addr,
) (conf *proxy.CustomUpstreamConfig) {
return nil
}
// UpdateCommonUpstreamConfig implements the [ClientsContainer] interface for
// EmptyClientsContainer.
func (EmptyClientsContainer) UpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig) {}
// ClearUpstreamCache implements the [ClientsContainer] interface for
// EmptyClientsContainer.
func (EmptyClientsContainer) ClearUpstreamCache() {}

View File

@@ -29,19 +29,6 @@ import (
"github.com/ameshkov/dnscrypt/v2"
)
// ClientsContainer provides information about preconfigured DNS clients.
type ClientsContainer interface {
// UpstreamConfigByID returns the custom upstream configuration for the
// client having id, using boot to initialize the one if necessary. It
// returns nil if there is no custom upstream configuration for the client.
// The id is expected to be either a string representation of an IP address
// or the ClientID.
UpstreamConfigByID(
id string,
boot upstream.Resolver,
) (conf *proxy.CustomUpstreamConfig, err error)
}
// Config represents the DNS filtering configuration of AdGuard Home. The zero
// Config is empty and ready for use.
type Config struct {
@@ -467,7 +454,7 @@ func (s *Server) prepareIpsetListSettings() (ipsets []string, err error) {
}
ipsets = stringutil.SplitTrimmed(string(data), "\n")
ipsets = slices.DeleteFunc(ipsets, IsCommentOrEmpty)
ipsets = slices.DeleteFunc(ipsets, aghnet.IsCommentOrEmpty)
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
@@ -478,7 +465,7 @@ func (s *Server) prepareIpsetListSettings() (ipsets []string, err error) {
// the configuration itself.
func (conf *ServerConfig) loadUpstreams() (upstreams []string, err error) {
if conf.UpstreamDNSFileName == "" {
return stringutil.FilterOut(conf.UpstreamDNS, IsCommentOrEmpty), nil
return stringutil.FilterOut(conf.UpstreamDNS, aghnet.IsCommentOrEmpty), nil
}
var data []byte
@@ -491,7 +478,7 @@ func (conf *ServerConfig) loadUpstreams() (upstreams []string, err error) {
log.Debug("dnsforward: got %d upstreams in %q", len(upstreams), conf.UpstreamDNSFileName)
return stringutil.FilterOut(upstreams, IsCommentOrEmpty), nil
return stringutil.FilterOut(upstreams, aghnet.IsCommentOrEmpty), nil
}
// collectListenAddr adds addrPort to addrs. It also adds its port to

View File

@@ -299,6 +299,7 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
UpstreamDNS: []string{upsAddr},
},
UsePrivateRDNS: true,
@@ -337,6 +338,7 @@ func TestServer_dns64WithDisabledRDNS(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
UpstreamDNS: []string{upsAddr},
},
UsePrivateRDNS: false,

View File

@@ -329,6 +329,14 @@ func (s *Server) AddrProcConfig() (c *client.DefaultAddrProcConfig) {
}
}
// UpstreamTimeout returns the current upstream timeout configuration.
func (s *Server) UpstreamTimeout() (t time.Duration) {
s.serverLock.RLock()
defer s.serverLock.RUnlock()
return s.conf.UpstreamTimeout
}
// Resolve gets IP addresses by host name from an upstream server. No
// request/response filtering is performed. Query log and Stats are not
// updated. This method may be called before [Server.Start].
@@ -532,7 +540,7 @@ func (s *Server) prepareUpstreamSettings(boot upstream.Resolver) (err error) {
uc, err := newUpstreamConfig(upstreams, defaultDNS, &upstream.Options{
Bootstrap: boot,
Timeout: s.conf.UpstreamTimeout,
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
HTTPVersions: aghnet.UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
PreferIPv6: s.conf.BootstrapPreferIPv6,
// Use a customized set of RootCAs, because Go's default mechanism of
// loading TLS roots does not always work properly on some routers so we're
@@ -549,6 +557,13 @@ func (s *Server) prepareUpstreamSettings(boot upstream.Resolver) (err error) {
}
s.conf.UpstreamConfig = uc
s.conf.ClientsContainer.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{
Bootstrap: boot,
UpstreamTimeout: s.conf.UpstreamTimeout,
BootstrapPreferIPv6: s.conf.BootstrapPreferIPv6,
EDNSClientSubnetEnabled: s.conf.EDNSClientSubnet.Enabled,
UseHTTP3Upstreams: s.conf.UseHTTP3Upstreams,
})
return nil
}
@@ -622,7 +637,7 @@ func (s *Server) prepareInternalDNS() (err error) {
bootOpts := &upstream.Options{
Timeout: DefaultTimeout,
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
HTTPVersions: aghnet.UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
}
s.bootstrap, s.bootResolvers, err = newBootstrap(s.conf.BootstrapDNS, s.etcHosts, bootOpts)
@@ -653,7 +668,7 @@ func (s *Server) prepareInternalDNS() (err error) {
// setupFallbackDNS initializes the fallback DNS servers.
func (s *Server) setupFallbackDNS() (uc *proxy.UpstreamConfig, err error) {
fallbacks := s.conf.FallbackDNS
fallbacks = stringutil.FilterOut(fallbacks, IsCommentOrEmpty)
fallbacks = stringutil.FilterOut(fallbacks, aghnet.IsCommentOrEmpty)
if len(fallbacks) == 0 {
return nil, nil
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
@@ -61,6 +62,42 @@ const (
// TODO(a.garipov): Use more.
var testClientAddrPort = netip.MustParseAddrPort("1.2.3.4:12345")
// type check
var _ ClientsContainer = (*clientsContainer)(nil)
// clientsContainer is a mock [ClientsContainer] implementation for tests.
type clientsContainer struct {
OnCustomUpstreamConfig func(
clientID string,
cliAddr netip.Addr,
) (conf *proxy.CustomUpstreamConfig)
OnUpdateCommonUpstreamConfig func(conf *client.CommonUpstreamConfig)
OnClearUpstreamCache func()
}
// CustomUpstreamConfig implements the [ClientsContainer] interface for
// *clientsContainer.
func (c *clientsContainer) CustomUpstreamConfig(
clientID string,
cliAddr netip.Addr,
) (conf *proxy.CustomUpstreamConfig) {
return c.OnCustomUpstreamConfig(clientID, cliAddr)
}
// UpdateCommonUpstreamConfig implements the [ClientsContainer] interface for
// *clientsContainer.
func (c *clientsContainer) UpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig) {
c.OnUpdateCommonUpstreamConfig(conf)
}
// ClearUpstreamCache implements the [ClientsContainer] interface for
// *clientsContainer.
func (c *clientsContainer) ClearUpstreamCache() {
c.OnClearUpstreamCache()
}
func startDeferStop(t *testing.T, s *Server) {
t.Helper()
@@ -168,6 +205,7 @@ func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte)
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})
@@ -297,6 +335,7 @@ func TestServer(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})
@@ -337,6 +376,7 @@ func TestServer_timeout(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -364,6 +404,7 @@ func TestServer_timeout(t *testing.T) {
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{
Enabled: false,
}
s.conf.Config.ClientsContainer = EmptyClientsContainer{}
err = s.Prepare(&s.conf)
require.NoError(t, err)
@@ -380,6 +421,7 @@ func TestServer_Prepare_fallbacks(t *testing.T) {
},
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -405,6 +447,7 @@ func TestServerWithProtectionDisabled(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})
@@ -536,6 +579,7 @@ func TestSafeSearch(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -629,6 +673,7 @@ func TestInvalidRequest(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})
@@ -659,6 +704,7 @@ func TestBlockedRequest(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -696,6 +742,7 @@ func TestServerCustomClientUpstream(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -721,12 +768,12 @@ func TestServerCustomClientUpstream(t *testing.T) {
forwardConf.EDNSClientSubnet.Enabled,
)
s.conf.ClientsContainer = &aghtest.ClientsContainer{
OnUpstreamConfigByID: func(
s.conf.ClientsContainer = &clientsContainer{
OnCustomUpstreamConfig: func(
_ string,
_ upstream.Resolver,
) (conf *proxy.CustomUpstreamConfig, err error) {
return customUpsConf, nil
_ netip.Addr,
) (conf *proxy.CustomUpstreamConfig) {
return customUpsConf
},
}
@@ -774,6 +821,7 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})
@@ -808,6 +856,7 @@ func TestBlockCNAME(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -884,6 +933,7 @@ func TestClientRulesForCNAMEMatching(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -930,6 +980,7 @@ func TestNullBlockedRequest(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -998,6 +1049,7 @@ func TestBlockedCustomIP(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -1051,6 +1103,7 @@ func TestBlockedByHosts(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -1103,6 +1156,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -1164,6 +1218,7 @@ func TestRewrite(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}))
@@ -1290,6 +1345,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
s.conf.Config.ClientsContainer = EmptyClientsContainer{}
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
err = s.Prepare(&s.conf)
@@ -1375,6 +1431,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
s.conf.Config.ClientsContainer = EmptyClientsContainer{}
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
err = s.Prepare(&s.conf)
@@ -1643,6 +1700,7 @@ func TestServer_Exchange(t *testing.T) {
UpstreamDNS: []string{upsAddr},
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
LocalPTRResolvers: []string{localUpsAddr},
UsePrivateRDNS: true,
@@ -1665,6 +1723,7 @@ func TestServer_Exchange(t *testing.T) {
UpstreamDNS: []string{upsAddr},
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
LocalPTRResolvers: []string{},
ServePlainDNS: true,

View File

@@ -40,6 +40,7 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})

View File

@@ -36,6 +36,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{
Enabled: false,
},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}

View File

@@ -11,6 +11,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
@@ -18,6 +19,7 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/validate"
)
// jsonDNSConfig is the JSON representation of the DNS server configuration.
@@ -53,6 +55,9 @@ type jsonDNSConfig struct {
// rate limiting requests.
RatelimitSubnetLenIPv6 *int `json:"ratelimit_subnet_len_ipv6"`
// UpstreamTimeout is an upstream timeout in seconds.
UpstreamTimeout *int `json:"upstream_timeout"`
// RatelimitWhitelist is a list of IP addresses excluded from rate limiting.
RatelimitWhitelist *[]netip.Addr `json:"ratelimit_whitelist"`
@@ -147,6 +152,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
ratelimitSubnetLenIPv4 := s.conf.RatelimitSubnetLenIPv4
ratelimitSubnetLenIPv6 := s.conf.RatelimitSubnetLenIPv6
ratelimitWhitelist := append([]netip.Addr{}, s.conf.RatelimitWhitelist...)
upstreamTimeout := int(s.conf.UpstreamTimeout.Seconds())
customIP := s.conf.EDNSClientSubnet.CustomIP
enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
@@ -192,6 +198,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
RatelimitSubnetLenIPv4: &ratelimitSubnetLenIPv4,
RatelimitSubnetLenIPv6: &ratelimitSubnetLenIPv6,
RatelimitWhitelist: &ratelimitWhitelist,
UpstreamTimeout: &upstreamTimeout,
EDNSCSCustomIP: customIP,
EDNSCSEnabled: &enableEDNSClientSubnet,
EDNSCSUseCustom: &useCustom,
@@ -302,6 +309,12 @@ func (req *jsonDNSConfig) validate(
return err
}
err = req.checkUpstreamTimeout()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
return nil
}
@@ -437,6 +450,16 @@ func (req *jsonDNSConfig) checkRatelimitSubnetMaskLen() (err error) {
return nil
}
// checkUpstreamTimeout returns an error if the configuration of the upstream
// timeout is invalid.
func (req *jsonDNSConfig) checkUpstreamTimeout() (err error) {
if req.UpstreamTimeout == nil {
return nil
}
return validate.NoLessThan("upstream_timeout", *req.UpstreamTimeout, 1)
}
// checkInclusion returns an error if a ptr is not nil and points to value,
// that not in the inclusive range between minN and maxN.
func checkInclusion(ptr *int, minN, maxN int) (err error) {
@@ -588,6 +611,14 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
shouldRestart = true
}
if dc.UpstreamTimeout != nil {
ut := time.Duration(*dc.UpstreamTimeout) * time.Second
if s.conf.UpstreamTimeout != ut {
s.conf.UpstreamTimeout = ut
shouldRestart = true
}
}
return shouldRestart
}
@@ -617,7 +648,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
return
}
req.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty)
req.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, aghnet.IsCommentOrEmpty)
opts := &upstream.Options{
Timeout: s.conf.UpstreamTimeout,
@@ -643,6 +674,8 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.
func (s *Server) handleCacheClear(w http.ResponseWriter, _ *http.Request) {
s.dnsProxy.ClearCache()
s.conf.ClientsContainer.ClearUpstreamCache()
_, _ = io.WriteString(w, "OK")
}

View File

@@ -83,6 +83,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
RatelimitSubnetLenIPv6: 56,
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ConfigModified: func() {},
ServePlainDNS: true,
@@ -164,6 +165,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
RatelimitSubnetLenIPv6: 56,
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ConfigModified: func() {},
ServePlainDNS: true,
@@ -299,24 +301,6 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
}
}
func TestIsCommentOrEmpty(t *testing.T) {
for _, tc := range []struct {
want assert.BoolAssertionFunc
str string
}{{
want: assert.True,
str: "",
}, {
want: assert.True,
str: "# comment",
}, {
want: assert.False,
str: "1.2.3.4",
}} {
tc.want(t, IsCommentOrEmpty(tc.str))
}
}
func newLocalUpstreamListener(t *testing.T, port uint16, handler dns.Handler) (real netip.AddrPort) {
t.Helper()
@@ -388,6 +372,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})

View File

@@ -1,7 +1,6 @@
package dnsforward
import (
"cmp"
"context"
"encoding/binary"
"net"
@@ -577,17 +576,14 @@ func (s *Server) setCustomUpstream(pctx *proxy.DNSContext, clientID string) {
return
}
// Use the ClientID first, since it has a higher priority.
id := cmp.Or(clientID, pctx.Addr.Addr().String())
upsConf, err := s.conf.ClientsContainer.UpstreamConfigByID(id, s.bootstrap)
if err != nil {
log.Error("dnsforward: getting custom upstreams for client %s: %s", id, err)
return
}
cliAddr := pctx.Addr.Addr()
upsConf := s.conf.ClientsContainer.CustomUpstreamConfig(clientID, cliAddr)
if upsConf != nil {
log.Debug("dnsforward: using custom upstreams for client %s", id)
log.Debug(
"dnsforward: using custom upstreams for client with ip %s and clientid %q",
cliAddr,
clientID,
)
pctx.CustomUpstreamConfig = upsConf
}

View File

@@ -81,6 +81,7 @@ func TestServer_ProcessInitial(t *testing.T) {
AAAADisabled: tc.aaaaDisabled,
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -180,6 +181,7 @@ func TestServer_ProcessFilteringAfterResponse(t *testing.T) {
AAAADisabled: tc.aaaaDisabled,
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
}
@@ -324,6 +326,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
HandleDDR: tc.ddrEnabled,
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
TLSConfig: TLSConfig{
ServerName: ddrTestDomainName,
@@ -660,6 +663,7 @@ func TestServer_HandleDNSRequest_restrictLocal(t *testing.T) {
UpstreamDNS: []string{localUpsAddr},
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
UsePrivateRDNS: true,
LocalPTRResolvers: []string{localUpsAddr},
@@ -788,6 +792,7 @@ func TestServer_ProcessUpstream_localPTR(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
UsePrivateRDNS: true,
LocalPTRResolvers: []string{localUpsAddr},
@@ -816,6 +821,7 @@ func TestServer_ProcessUpstream_localPTR(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
UsePrivateRDNS: false,
LocalPTRResolvers: []string{localUpsAddr},

View File

@@ -122,9 +122,14 @@ func (s *Server) logQuery(dctx *dnsContext, ip net.IP, processingTime time.Durat
if pctx.Upstream != nil {
p.Upstream = pctx.Upstream.Address()
} else if cachedUps := pctx.CachedUpstreamAddr; cachedUps != "" {
p.Upstream = pctx.CachedUpstreamAddr
p.Cached = true
}
if qs := pctx.QueryStatistics(); qs != nil {
ms := qs.Main()
if len(ms) == 1 && ms[0].IsCached {
p.Upstream = ms[0].Address
p.Cached = true
}
}
s.queryLog.Add(p)
@@ -134,15 +139,18 @@ func (s *Server) logQuery(dctx *dnsContext, ip net.IP, processingTime time.Durat
func (s *Server) updateStats(dctx *dnsContext, clientIP string, processingTime time.Duration) {
pctx := dctx.proxyCtx
var upstreamStats []*proxy.UpstreamStatistics
qs := pctx.QueryStatistics()
if qs != nil {
upstreamStats = append(upstreamStats, qs.Main()...)
upstreamStats = append(upstreamStats, qs.Fallback()...)
}
e := &stats.Entry{
UpstreamStats: upstreamStats,
Domain: aghnet.NormalizeDomain(pctx.Req.Question[0].Name),
Result: stats.RNotFiltered,
ProcessingTime: processingTime,
UpstreamTime: pctx.QueryDuration,
}
if pctx.Upstream != nil {
e.Upstream = pctx.Upstream.Address()
}
if clientID := dctx.clientID; clientID != "" {

View File

@@ -19,6 +19,7 @@ func TestGenAnswerHTTPS_andSVCB(t *testing.T) {
Config: Config{
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
ClientsContainer: EmptyClientsContainer{},
},
ServePlainDNS: true,
})

View File

@@ -24,6 +24,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -63,6 +64,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -102,6 +104,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,

View File

@@ -29,6 +29,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -70,6 +71,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -112,6 +114,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -154,6 +157,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -196,6 +200,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -240,6 +245,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -285,6 +291,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -327,6 +334,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": true,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -371,6 +379,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": true,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -415,6 +424,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -457,6 +467,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": true,
"disable_ipv6": false,
@@ -499,6 +510,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -541,6 +553,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -583,6 +596,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -627,6 +641,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -671,6 +686,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -714,6 +730,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -756,6 +773,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -800,6 +818,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -847,6 +866,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -889,6 +909,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -935,6 +956,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -977,6 +999,7 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 11,
"upstream_timeout": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
@@ -1022,6 +1045,50 @@
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 10,
"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": [],
"edns_cs_use_custom": false,
"edns_cs_custom_ip": ""
}
},
"upstream_timeout": {
"req": {
"upstream_timeout": 11
},
"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"
],
"fallback_dns": [],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"ratelimit_subnet_len_ipv4": 24,
"ratelimit_subnet_len_ipv6": 56,
"ratelimit_whitelist": [],
"blocking_mode": "default",
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"upstream_timeout": 11,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,

View File

@@ -94,7 +94,7 @@ func newPrivateConfig(
) (uc *proxy.UpstreamConfig, err error) {
confNeedsFiltering := len(addrs) > 0
if confNeedsFiltering {
addrs = stringutil.FilterOut(addrs, IsCommentOrEmpty)
addrs = stringutil.FilterOut(addrs, aghnet.IsCommentOrEmpty)
} else {
sysResolvers := slices.DeleteFunc(slices.Clone(sysResolvers.Addrs()), unwanted.Has)
addrs = make([]string, 0, len(sysResolvers))
@@ -127,20 +127,6 @@ func newPrivateConfig(
return uc, nil
}
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
if !http3 {
return upstream.DefaultHTTPVersions
}
return []upstream.HTTPVersion{
upstream.HTTPVersion3,
upstream.HTTPVersion2,
upstream.HTTPVersion11,
}
}
// setProxyUpstreamMode sets the upstream mode and related settings in conf
// based on provided parameters.
func setProxyUpstreamMode(
@@ -162,10 +148,3 @@ func setProxyUpstreamMode(
return nil
}
// IsCommentOrEmpty returns true if s starts with a "#" character or is empty.
// This function is useful for filtering out non-upstream lines from upstream
// configs.
func IsCommentOrEmpty(s string) (ok bool) {
return len(s) == 0 || s[0] == '#'
}

View File

@@ -18,7 +18,6 @@ func TestFilter_Refresh(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
uid := rulelist.MustNewUID()
const fltData = testRuleTextTitle + testRuleTextBlocked
fileURL, srvURL := newFilterLocations(t, cacheDir, fltData, fltData)
@@ -54,6 +53,7 @@ func TestFilter_Refresh(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
uid := rulelist.MustNewUID()
f, err := rulelist.NewFilter(&rulelist.FilterConfig{
URL: tc.url,
Name: tc.name,

View File

@@ -356,7 +356,7 @@ func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
// There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth()
if ok {
u, _ = Context.auth.findUser(user, pass)
u, _ = globalContext.auth.findUser(user, pass)
return u
}

View File

@@ -155,7 +155,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
if rateLimiter := Context.auth.rateLimiter; rateLimiter != nil {
if rateLimiter := globalContext.auth.rateLimiter; rateLimiter != nil {
if left := rateLimiter.check(remoteIP); left > 0 {
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
writeErrorWithIP(
@@ -176,10 +176,10 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
}
cookie, err := Context.auth.newCookie(req, remoteIP)
cookie, err := globalContext.auth.newCookie(req, remoteIP)
if err != nil {
logIP := remoteIP
if Context.auth.trustedProxies.Contains(ip.Unmap()) {
if globalContext.auth.trustedProxies.Contains(ip.Unmap()) {
logIP = ip.String()
}
@@ -213,7 +213,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
return
}
Context.auth.removeSession(c.Value)
globalContext.auth.removeSession(c.Value)
c = &http.Cookie{
Name: sessionCookieName,
@@ -232,7 +232,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
// RegisterAuthHandlers - register handlers
func RegisterAuthHandlers() {
Context.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin)))
globalContext.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin)))
httpRegister(http.MethodGet, "/control/logout", handleLogout)
}
@@ -254,13 +254,13 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
// Check Basic authentication.
user, pass, hasBasic := r.BasicAuth()
if hasBasic {
_, isAuthenticated = Context.auth.findUser(user, pass)
_, isAuthenticated = globalContext.auth.findUser(user, pass)
if !isAuthenticated {
log.Info("%s: invalid basic authorization value", pref)
}
}
} else {
res := Context.auth.checkSession(cookie.Value)
res := globalContext.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("%s: invalid cookie value: %q", pref, cookie)
@@ -294,12 +294,12 @@ func optionalAuth(
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
authRequired := Context.auth != nil && Context.auth.authRequired()
authRequired := globalContext.auth != nil && globalContext.auth.authRequired()
if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil {
// Redirect to the dashboard if already authenticated.
res := Context.auth.checkSession(cookie.Value)
res := globalContext.auth.checkSession(cookie.Value)
if res == checkSessionOK {
http.Redirect(w, r, "", http.StatusFound)

View File

@@ -39,7 +39,7 @@ func TestAuthHTTP(t *testing.T) {
users := []webUser{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
}
Context.auth = InitAuth(fn, users, 60, nil, nil)
globalContext.auth = InitAuth(fn, users, 60, nil, nil)
handlerCalled := false
handler := func(_ http.ResponseWriter, _ *http.Request) {
@@ -68,7 +68,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)
// perform login
cookie, err := Context.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "")
cookie, err := globalContext.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "")
require.NoError(t, err)
require.NotNil(t, cookie)
@@ -114,7 +114,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)
r.Header.Del(httphdr.Cookie)
Context.auth.Close()
globalContext.auth.Close()
}
func TestRealIP(t *testing.T) {

View File

@@ -12,17 +12,14 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil"
)
// clientsContainer is the storage of all runtime and persistent clients.
@@ -75,6 +72,7 @@ func (clients *clientsContainer) Init(
etcHosts *aghnet.HostsContainer,
arpDB arpdb.Interface,
filteringConf *filtering.Config,
sigHdlr *signalHandler,
) (err error) {
// TODO(s.chzhen): Refactor it.
if clients.storage != nil {
@@ -109,6 +107,7 @@ func (clients *clientsContainer) Init(
clients.storage, err = client.NewStorage(ctx, &client.StorageConfig{
Logger: baseLogger.With(slogutil.KeyPrefix, "client_storage"),
Clock: timeutil.SystemClock{},
InitialClients: confClients,
DHCP: dhcpServer,
EtcHosts: hosts,
@@ -120,6 +119,8 @@ func (clients *clientsContainer) Init(
return fmt.Errorf("init client storage: %w", err)
}
sigHdlr.addClientStorage(clients.storage)
return nil
}
@@ -370,63 +371,6 @@ func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
return true
}
// type check
var _ dnsforward.ClientsContainer = (*clientsContainer)(nil)
// UpstreamConfigByID implements the [dnsforward.ClientsContainer] interface for
// *clientsContainer. upsConf is nil if the client isn't found or if the client
// has no custom upstreams.
func (clients *clientsContainer) UpstreamConfigByID(
id string,
bootstrap upstream.Resolver,
) (conf *proxy.CustomUpstreamConfig, err error) {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.storage.Find(id)
if !ok {
return nil, nil
} else if c.UpstreamConfig != nil {
return c.UpstreamConfig, nil
}
upstreams := stringutil.FilterOut(c.Upstreams, dnsforward.IsCommentOrEmpty)
if len(upstreams) == 0 {
return nil, nil
}
var upsConf *proxy.UpstreamConfig
upsConf, err = proxy.ParseUpstreamsConfig(
upstreams,
&upstream.Options{
Bootstrap: bootstrap,
Timeout: time.Duration(config.DNS.UpstreamTimeout),
HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
PreferIPv6: config.DNS.BootstrapPreferIPv6,
},
)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
conf = proxy.NewCustomUpstreamConfig(
upsConf,
c.UpstreamsCacheEnabled,
int(c.UpstreamsCacheSize),
config.DNS.EDNSClientSubnet.Enabled,
)
c.UpstreamConfig = conf
// TODO(s.chzhen): Pass context.
err = clients.storage.Update(context.TODO(), c.Name, c)
if err != nil {
return nil, fmt.Errorf("setting upstream config: %w", err)
}
return conf, nil
}
// type check
var _ client.AddressUpdater = (*clientsContainer)(nil)

View File

@@ -1,15 +1,12 @@
package home
import (
"net"
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -31,34 +28,10 @@ func newClientsContainer(t *testing.T) (c *clientsContainer) {
nil,
nil,
&filtering.Config{},
newSignalHandler(nil, nil),
)
require.NoError(t, err)
return c
}
func TestClientsCustomUpstream(t *testing.T) {
clients := newClientsContainer(t)
ctx := testutil.ContextWithTimeout(t, testTimeout)
// Add client with upstreams.
err := clients.storage.Add(ctx, &client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1:2:3::4")},
Upstreams: []string{
"1.1.1.1",
"[/example.org/]8.8.8.8",
},
})
require.NoError(t, err)
upsConf, err := clients.UpstreamConfigByID("1.2.3.4", net.DefaultResolver)
assert.Nil(t, upsConf)
assert.NoError(t, err)
upsConf, err = clients.UpstreamConfigByID("1.1.1.1", net.DefaultResolver)
require.NotNil(t, upsConf)
assert.NoError(t, err)
}

View File

@@ -486,9 +486,9 @@ var config = &configuration{
// configFilePath returns the absolute path to the symlink-evaluated path to the
// current config file.
func configFilePath() (confPath string) {
confPath, err := filepath.EvalSymlinks(Context.confFilePath)
confPath, err := filepath.EvalSymlinks(globalContext.confFilePath)
if err != nil {
confPath = Context.confFilePath
confPath = globalContext.confFilePath
logFunc := log.Error
if errors.Is(err, os.ErrNotExist) {
logFunc = log.Debug
@@ -498,7 +498,7 @@ func configFilePath() (confPath string) {
}
if !filepath.IsAbs(confPath) {
confPath = filepath.Join(Context.workDir, confPath)
confPath = filepath.Join(globalContext.workDir, confPath)
}
return confPath
@@ -530,8 +530,8 @@ func parseConfig() (err error) {
}
migrator := configmigrate.New(&configmigrate.Config{
WorkingDir: Context.workDir,
DataDir: Context.getDataDir(),
WorkingDir: globalContext.workDir,
DataDir: globalContext.getDataDir(),
})
var upgraded bool
@@ -644,27 +644,27 @@ func (c *configuration) write() (err error) {
c.Lock()
defer c.Unlock()
if Context.auth != nil {
config.Users = Context.auth.usersList()
if globalContext.auth != nil {
config.Users = globalContext.auth.usersList()
}
if Context.tls != nil {
if globalContext.tls != nil {
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
globalContext.tls.WriteDiskConfig(&tlsConf)
config.TLS = tlsConf
}
if Context.stats != nil {
if globalContext.stats != nil {
statsConf := stats.Config{}
Context.stats.WriteDiskConfig(&statsConf)
globalContext.stats.WriteDiskConfig(&statsConf)
config.Stats.Interval = timeutil.Duration(statsConf.Limit)
config.Stats.Enabled = statsConf.Enabled
config.Stats.Ignored = statsConf.Ignored.Values()
}
if Context.queryLog != nil {
if globalContext.queryLog != nil {
dc := querylog.Config{}
Context.queryLog.WriteDiskConfig(&dc)
globalContext.queryLog.WriteDiskConfig(&dc)
config.DNS.AnonymizeClientIP = dc.AnonymizeClientIP
config.QueryLog.Enabled = dc.Enabled
config.QueryLog.FileEnabled = dc.FileEnabled
@@ -673,14 +673,14 @@ func (c *configuration) write() (err error) {
config.QueryLog.Ignored = dc.Ignored.Values()
}
if Context.filters != nil {
Context.filters.WriteDiskConfig(config.Filtering)
if globalContext.filters != nil {
globalContext.filters.WriteDiskConfig(config.Filtering)
config.Filters = config.Filtering.Filters
config.WhitelistFilters = config.Filtering.WhitelistFilters
config.UserRules = config.Filtering.UserRules
}
if s := Context.dnsServer; s != nil {
if s := globalContext.dnsServer; s != nil {
c := dnsforward.Config{}
s.WriteDiskConfig(&c)
dns := &config.DNS
@@ -692,13 +692,14 @@ func (c *configuration) write() (err error) {
config.Clients.Sources.RDNS = addrProcConf.UseRDNS
config.Clients.Sources.WHOIS = addrProcConf.UseWHOIS
dns.UsePrivateRDNS = addrProcConf.UsePrivateRDNS
dns.UpstreamTimeout = timeutil.Duration(s.UpstreamTimeout())
}
if Context.dhcpServer != nil {
Context.dhcpServer.WriteDiskConfig(config.DHCP)
if globalContext.dhcpServer != nil {
globalContext.dhcpServer.WriteDiskConfig(config.DHCP)
}
config.Clients.Persistent = Context.clients.forConfig()
config.Clients.Persistent = globalContext.clients.forConfig()
confPath := configFilePath()
log.Debug("writing config file %q", confPath)
@@ -725,14 +726,14 @@ func setContextTLSCipherIDs() (err error) {
if len(config.TLS.OverrideTLSCiphers) == 0 {
log.Info("tls: using default ciphers")
Context.tlsCipherIDs = aghtls.SaferCipherSuites()
globalContext.tlsCipherIDs = aghtls.SaferCipherSuites()
return nil
}
log.Info("tls: overriding ciphers: %s", config.TLS.OverrideTLSCiphers)
Context.tlsCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
globalContext.tlsCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
if err != nil {
return fmt.Errorf("parsing override ciphers: %w", err)
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/urlutil"
"github.com/NYTimes/gziphandler"
@@ -129,10 +128,10 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
protectionDisabledUntil *time.Time
protectionEnabled bool
)
if Context.dnsServer != nil {
if globalContext.dnsServer != nil {
fltConf = &dnsforward.Config{}
Context.dnsServer.WriteDiskConfig(fltConf)
protectionEnabled, protectionDisabledUntil = Context.dnsServer.UpdatedProtectionStatus()
globalContext.dnsServer.WriteDiskConfig(fltConf)
protectionEnabled, protectionDisabledUntil = globalContext.dnsServer.UpdatedProtectionStatus()
}
var resp statusResponse
@@ -162,7 +161,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
// IsDHCPAvailable field is now false by default for Windows.
if runtime.GOOS != "windows" {
resp.IsDHCPAvailable = Context.dhcpServer != nil
resp.IsDHCPAvailable = globalContext.dhcpServer != nil
}
aghhttp.WriteJSONResponseOK(w, r, resp)
@@ -172,7 +171,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
// registration of handlers
// ------------------------
func registerControlHandlers(web *webAPI) {
Context.mux.HandleFunc(
globalContext.mux.HandleFunc(
"/control/version.json",
postInstall(optionalAuth(web.handleVersionJSON)),
)
@@ -185,19 +184,19 @@ func registerControlHandlers(web *webAPI) {
httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile)
// No auth is necessary for DoH/DoT configurations
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
Context.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDoT))
globalContext.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
globalContext.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDoT))
RegisterAuthHandlers()
}
func httpRegister(method, url string, handler http.HandlerFunc) {
if method == "" {
// "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method
Context.mux.HandleFunc(url, postInstall(handler))
globalContext.mux.HandleFunc(url, postInstall(handler))
return
}
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
globalContext.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
}
// ensure returns a wrapped handler that makes sure that the request has the
@@ -207,11 +206,7 @@ func ensure(
handler func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
m, u := r.Method, r.URL
log.Debug("started %s %s %s", m, r.Host, u)
defer func() { log.Debug("finished %s %s %s in %s", m, r.Host, u, time.Since(start)) }()
m := r.Method
if m != method {
aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only method %s is allowed", method)
@@ -223,8 +218,8 @@ func ensure(
return
}
Context.controlLock.Lock()
defer Context.controlLock.Unlock()
globalContext.controlLock.Lock()
defer globalContext.controlLock.Unlock()
}
handler(w, r)
@@ -293,7 +288,7 @@ func ensureHandler(method string, handler func(http.ResponseWriter, *http.Reques
// preInstall lets the handler run only if firstRun is true, no redirects
func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !Context.firstRun {
if !globalContext.firstRun {
// if it's not first run, don't let users access it (for example /install.html when configuration is done)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
@@ -320,7 +315,7 @@ func preInstallHandler(handler http.Handler) http.Handler {
// HTTPS-related headers. If proceed is true, the middleware must continue
// handling the request.
func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (proceed bool) {
web := Context.web
web := globalContext.web
if web.httpsServer.server == nil {
return true
}
@@ -409,7 +404,7 @@ func httpsURL(u *url.URL, host string, portHTTPS uint16) (redirectURL *url.URL)
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
if globalContext.firstRun && !strings.HasPrefix(path, "/install.") &&
!strings.HasPrefix(path, "/assets/") {
http.Redirect(w, r, "install.html", http.StatusFound)

View File

@@ -428,20 +428,20 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
curConfig := &configuration{}
copyInstallSettings(curConfig, config)
Context.firstRun = false
globalContext.firstRun = false
config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
config.DNS.Port = req.DNS.Port
config.Filtering.SafeFSPatterns = []string{
filepath.Join(Context.workDir, userFilterDataDir, "*"),
filepath.Join(globalContext.workDir, userFilterDataDir, "*"),
}
config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, req.Web.Port)
u := &webUser{
Name: req.Username,
}
err = Context.auth.addUser(u, req.Password)
err = globalContext.auth.addUser(u, req.Password)
if err != nil {
Context.firstRun = true
globalContext.firstRun = true
copyInstallSettings(config, curConfig)
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "%s", err)
@@ -454,7 +454,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
// functions potentially restart the HTTPS server.
err = startMods(web.baseLogger)
if err != nil {
Context.firstRun = true
globalContext.firstRun = true
copyInstallSettings(config, curConfig)
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
@@ -463,7 +463,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
err = config.write()
if err != nil {
Context.firstRun = true
globalContext.firstRun = true
copyInstallSettings(config, curConfig)
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write config: %s", err)
@@ -528,7 +528,7 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
}
func (web *webAPI) registerInstallHandlers() {
Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))
globalContext.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
globalContext.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
globalContext.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))
}

View File

@@ -165,7 +165,7 @@ func (vr *versionResponse) setAllowedToAutoUpdate() (err error) {
}
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
globalContext.tls.WriteDiskConfig(tlsConf)
canUpdate := true
if tlsConfUsesPrivilegedPorts(tlsConf) ||

View File

@@ -45,9 +45,9 @@ func onConfigModified() {
}
}
// initDNS updates all the fields of the [Context] needed to initialize the DNS
// initDNS updates all the fields of the [globalContext] needed to initialize the DNS
// server and initializes it at last. It also must not be called unless
// [config] and [Context] are initialized. baseLogger must not be nil.
// [config] and [globalContext] are initialized. baseLogger must not be nil.
func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error) {
anonymizer := config.anonymizer()
@@ -58,7 +58,7 @@ func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error)
ConfigModified: onConfigModified,
HTTPRegister: httpRegister,
Enabled: config.Stats.Enabled,
ShouldCountClient: Context.clients.shouldCountClient,
ShouldCountClient: globalContext.clients.shouldCountClient,
}
engine, err := aghnet.NewIgnoreEngine(config.Stats.Ignored)
@@ -67,7 +67,7 @@ func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error)
}
statsConf.Ignored = engine
Context.stats, err = stats.New(statsConf)
globalContext.stats, err = stats.New(statsConf)
if err != nil {
return fmt.Errorf("init stats: %w", err)
}
@@ -77,7 +77,7 @@ func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error)
Anonymizer: anonymizer,
ConfigModified: onConfigModified,
HTTPRegister: httpRegister,
FindClient: Context.clients.findMultiple,
FindClient: globalContext.clients.findMultiple,
BaseDir: querylogDir,
AnonymizeClientIP: config.DNS.AnonymizeClientIP,
RotationIvl: time.Duration(config.QueryLog.Interval),
@@ -92,25 +92,25 @@ func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error)
}
conf.Ignored = engine
Context.queryLog, err = querylog.New(conf)
globalContext.queryLog, err = querylog.New(conf)
if err != nil {
return fmt.Errorf("init querylog: %w", err)
}
Context.filters, err = filtering.New(config.Filtering, nil)
globalContext.filters, err = filtering.New(config.Filtering, nil)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return err
}
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
globalContext.tls.WriteDiskConfig(tlsConf)
return initDNSServer(
Context.filters,
Context.stats,
Context.queryLog,
Context.dhcpServer,
globalContext.filters,
globalContext.stats,
globalContext.queryLog,
globalContext.dhcpServer,
anonymizer,
httpRegister,
tlsConf,
@@ -121,7 +121,7 @@ func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error)
// initDNSServer initializes the [context.dnsServer]. To only use the internal
// proxy, none of the arguments are required, but tlsConf and l still must not
// be nil, in other cases all the arguments also must not be nil. It also must
// not be called unless [config] and [Context] are initialized.
// not be called unless [config] and [globalContext] are initialized.
//
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
func initDNSServer(
@@ -134,7 +134,7 @@ func initDNSServer(
tlsConf *tlsConfigSettings,
l *slog.Logger,
) (err error) {
Context.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
globalContext.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
Logger: l,
DNSFilter: filters,
Stats: sts,
@@ -142,7 +142,7 @@ func initDNSServer(
PrivateNets: parseSubnetSet(config.DNS.PrivateNets),
Anonymizer: anonymizer,
DHCPServer: dhcpSrv,
EtcHosts: Context.etcHosts,
EtcHosts: globalContext.etcHosts,
LocalDomain: config.DHCP.LocalDomainName,
})
defer func() {
@@ -154,21 +154,27 @@ func initDNSServer(
return fmt.Errorf("dnsforward.NewServer: %w", err)
}
Context.clients.clientChecker = Context.dnsServer
globalContext.clients.clientChecker = globalContext.dnsServer
dnsConf, err := newServerConfig(&config.DNS, config.Clients.Sources, tlsConf, httpReg)
dnsConf, err := newServerConfig(
&config.DNS,
config.Clients.Sources,
tlsConf,
httpReg,
globalContext.clients.storage,
)
if err != nil {
return fmt.Errorf("newServerConfig: %w", err)
}
// Try to prepare the server with disabled private RDNS resolution if it
// failed to prepare as is. See TODO on [dnsforward.PrivateRDNSError].
err = Context.dnsServer.Prepare(dnsConf)
err = globalContext.dnsServer.Prepare(dnsConf)
if privRDNSErr := (&dnsforward.PrivateRDNSError{}); errors.As(err, &privRDNSErr) {
log.Info("WARNING: %s; trying to disable private RDNS resolution", err)
dnsConf.UsePrivateRDNS = false
err = Context.dnsServer.Prepare(dnsConf)
err = globalContext.dnsServer.Prepare(dnsConf)
}
if err != nil {
@@ -194,7 +200,7 @@ func parseSubnetSet(nets []netutil.Prefix) (s netutil.SubnetSet) {
}
func isRunning() bool {
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
return globalContext.dnsServer != nil && globalContext.dnsServer.IsRunning()
}
func ipsToTCPAddrs(ips []netip.Addr, port uint16) (tcpAddrs []*net.TCPAddr) {
@@ -230,12 +236,13 @@ func newServerConfig(
clientSrcConf *clientSourcesConfig,
tlsConf *tlsConfigSettings,
httpReg aghhttp.RegisterFunc,
clientsContainer dnsforward.ClientsContainer,
) (newConf *dnsforward.ServerConfig, err error) {
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
fwdConf := dnsConf.Config
fwdConf.FilterHandler = applyAdditionalFiltering
fwdConf.ClientsContainer = &Context.clients
fwdConf.ClientsContainer = clientsContainer
newConf = &dnsforward.ServerConfig{
UDPListenAddrs: ipsToUDPAddrs(hosts, dnsConf.Port),
@@ -244,7 +251,7 @@ func newServerConfig(
TLSConfig: newDNSTLSConfig(tlsConf, hosts),
TLSAllowUnencryptedDoH: tlsConf.AllowUnencryptedDoH,
UpstreamTimeout: time.Duration(dnsConf.UpstreamTimeout),
TLSv12Roots: Context.tlsRoots,
TLSv12Roots: globalContext.tlsRoots,
ConfigModified: onConfigModified,
HTTPRegister: httpReg,
LocalPTRResolvers: dnsConf.PrivateRDNSResolvers,
@@ -259,16 +266,16 @@ func newServerConfig(
var initialAddresses []netip.Addr
// Context.stats may be nil here if initDNSServer is called from
// [cmdlineUpdate].
if sts := Context.stats; sts != nil {
if sts := globalContext.stats; sts != nil {
const initialClientsNum = 100
initialAddresses = Context.stats.TopClientsIP(initialClientsNum)
initialAddresses = globalContext.stats.TopClientsIP(initialClientsNum)
}
// Do not set DialContext, PrivateSubnets, and UsePrivateRDNS, because they
// are set by [dnsforward.Server.Prepare].
newConf.AddrProcConf = &client.DefaultAddrProcConfig{
Exchanger: Context.dnsServer,
AddressUpdater: &Context.clients,
Exchanger: globalContext.dnsServer,
AddressUpdater: &globalContext.clients,
InitialAddresses: initialAddresses,
CatchPanics: true,
UseRDNS: clientSrcConf.RDNS,
@@ -359,7 +366,7 @@ type dnsEncryption struct {
func getDNSEncryption() (de dnsEncryption) {
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
globalContext.tls.WriteDiskConfig(&tlsConf)
if !tlsConf.Enabled || len(tlsConf.ServerName) == 0 {
return dnsEncryption{}
@@ -402,7 +409,7 @@ func applyAdditionalFiltering(clientIP netip.Addr, clientID string, setts *filte
// pref is a prefix for logging messages around the scope.
const pref = "applying filters"
Context.filters.ApplyBlockedServices(setts)
globalContext.filters.ApplyBlockedServices(setts)
log.Debug("%s: looking for client with ip %s and clientid %q", pref, clientIP, clientID)
@@ -412,9 +419,9 @@ func applyAdditionalFiltering(clientIP netip.Addr, clientID string, setts *filte
setts.ClientIP = clientIP
c, ok := Context.clients.storage.Find(clientID)
c, ok := globalContext.clients.storage.Find(clientID)
if !ok {
c, ok = Context.clients.storage.Find(clientIP.String())
c, ok = globalContext.clients.storage.Find(clientIP.String())
if !ok {
log.Debug("%s: no clients with ip %s and clientid %q", pref, clientIP, clientID)
@@ -429,7 +436,7 @@ func applyAdditionalFiltering(clientIP netip.Addr, clientID string, setts *filte
setts.ServicesRules = nil
svcs := c.BlockedServices.IDs
if !c.BlockedServices.Schedule.Contains(time.Now()) {
Context.filters.ApplyBlockedServicesList(setts, svcs)
globalContext.filters.ApplyBlockedServicesList(setts, svcs)
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
}
}
@@ -455,24 +462,24 @@ func startDNSServer() error {
return fmt.Errorf("unable to start forwarding DNS server: Already running")
}
Context.filters.EnableFilters(false)
globalContext.filters.EnableFilters(false)
// TODO(s.chzhen): Pass context.
ctx := context.TODO()
err := Context.clients.Start(ctx)
err := globalContext.clients.Start(ctx)
if err != nil {
return fmt.Errorf("starting clients container: %w", err)
}
err = Context.dnsServer.Start()
err = globalContext.dnsServer.Start()
if err != nil {
return fmt.Errorf("starting dns server: %w", err)
}
Context.filters.Start()
Context.stats.Start()
globalContext.filters.Start()
globalContext.stats.Start()
err = Context.queryLog.Start(ctx)
err = globalContext.queryLog.Start(ctx)
if err != nil {
return fmt.Errorf("starting query log: %w", err)
}
@@ -482,14 +489,20 @@ func startDNSServer() error {
func reconfigureDNSServer() (err error) {
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
globalContext.tls.WriteDiskConfig(tlsConf)
newConf, err := newServerConfig(&config.DNS, config.Clients.Sources, tlsConf, httpRegister)
newConf, err := newServerConfig(
&config.DNS,
config.Clients.Sources,
tlsConf,
httpRegister,
globalContext.clients.storage,
)
if err != nil {
return fmt.Errorf("generating forwarding dns server config: %w", err)
}
err = Context.dnsServer.Reconfigure(newConf)
err = globalContext.dnsServer.Reconfigure(newConf)
if err != nil {
return fmt.Errorf("starting forwarding dns server: %w", err)
}
@@ -502,12 +515,12 @@ func stopDNSServer() (err error) {
return nil
}
err = Context.dnsServer.Stop()
err = globalContext.dnsServer.Stop()
if err != nil {
return fmt.Errorf("stopping forwarding dns server: %w", err)
}
err = Context.clients.close(context.TODO())
err = globalContext.clients.close(context.TODO())
if err != nil {
return fmt.Errorf("closing clients container: %w", err)
}
@@ -519,25 +532,25 @@ func stopDNSServer() (err error) {
func closeDNSServer() {
// DNS forward module must be closed BEFORE stats or queryLog because it depends on them
if Context.dnsServer != nil {
Context.dnsServer.Close()
Context.dnsServer = nil
if globalContext.dnsServer != nil {
globalContext.dnsServer.Close()
globalContext.dnsServer = nil
}
if Context.filters != nil {
Context.filters.Close()
if globalContext.filters != nil {
globalContext.filters.Close()
}
if Context.stats != nil {
err := Context.stats.Close()
if globalContext.stats != nil {
err := globalContext.stats.Close()
if err != nil {
log.Error("closing stats: %s", err)
}
}
if Context.queryLog != nil {
if globalContext.queryLog != nil {
// TODO(s.chzhen): Pass context.
err := Context.queryLog.Shutdown(context.TODO())
err := globalContext.queryLog.Shutdown(context.TODO())
if err != nil {
log.Error("closing query log: %s", err)
}

View File

@@ -37,14 +37,14 @@ func newStorage(tb testing.TB, clients []*client.Persistent) (s *client.Storage)
func TestApplyAdditionalFiltering(t *testing.T) {
var err error
Context.filters, err = filtering.New(&filtering.Config{
globalContext.filters, err = filtering.New(&filtering.Config{
BlockedServices: &filtering.BlockedServices{
Schedule: schedule.EmptyWeekly(),
},
}, nil)
require.NoError(t, err)
Context.clients.storage = newStorage(t, []*client.Persistent{{
globalContext.clients.storage = newStorage(t, []*client.Persistent{{
Name: "default",
ClientIDs: []string{"default"},
UseOwnSettings: false,
@@ -124,7 +124,7 @@ func TestApplyAdditionalFiltering_blockedServices(t *testing.T) {
err error
)
Context.filters, err = filtering.New(&filtering.Config{
globalContext.filters, err = filtering.New(&filtering.Config{
BlockedServices: &filtering.BlockedServices{
Schedule: schedule.EmptyWeekly(),
IDs: globalBlockedServices,
@@ -132,7 +132,7 @@ func TestApplyAdditionalFiltering_blockedServices(t *testing.T) {
}, nil)
require.NoError(t, err)
Context.clients.storage = newStorage(t, []*client.Persistent{{
globalContext.clients.storage = newStorage(t, []*client.Persistent{{
Name: "default",
ClientIDs: []string{"default"},
UseOwnBlockedServices: false,

View File

@@ -91,10 +91,10 @@ func (c *homeContext) getDataDir() string {
return filepath.Join(c.workDir, dataDir)
}
// Context - a global context object
// globalContext is a global context object.
//
// TODO(a.garipov): Refactor.
var Context homeContext
var globalContext homeContext
// Main is the entry point
func Main(clientBuildFS fs.FS) {
@@ -113,40 +113,32 @@ func Main(clientBuildFS fs.FS) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
ctx := context.Background()
for {
sig := <-signals
log.Info("Received signal %q", sig)
switch sig {
case syscall.SIGHUP:
Context.clients.storage.ReloadARP(ctx)
Context.tls.reload()
default:
cleanup(ctx)
cleanupAlways()
close(done)
}
}
}()
ctx := context.Background()
sigHdlr := newSignalHandler(signals, func(ctx context.Context) {
cleanup(ctx)
cleanupAlways()
close(done)
})
go sigHdlr.handle(ctx)
if opts.serviceControlAction != "" {
handleServiceControlAction(opts, clientBuildFS, signals, done)
handleServiceControlAction(opts, clientBuildFS, signals, done, sigHdlr)
return
}
// run the protection
run(opts, clientBuildFS, done)
run(opts, clientBuildFS, done, sigHdlr)
}
// setupContext initializes [Context] fields. It also reads and upgrades
// setupContext initializes [globalContext] fields. It also reads and upgrades
// config file if necessary.
func setupContext(opts options) (err error) {
Context.firstRun = detectFirstRun()
globalContext.firstRun = detectFirstRun()
Context.tlsRoots = aghtls.SystemRootCAs()
Context.mux = http.NewServeMux()
globalContext.tlsRoots = aghtls.SystemRootCAs()
globalContext.mux = http.NewServeMux()
if !opts.noEtcHosts {
err = setupHostsContainer()
@@ -156,7 +148,7 @@ func setupContext(opts options) (err error) {
}
}
if Context.firstRun {
if globalContext.firstRun {
log.Info("This is the first time AdGuard Home is launched")
checkNetworkPermissions()
@@ -247,7 +239,7 @@ func setupHostsContainer() (err error) {
return fmt.Errorf("getting default system hosts paths: %w", err)
}
Context.etcHosts, err = aghnet.NewHostsContainer(osutil.RootDirFS(), hostsWatcher, paths...)
globalContext.etcHosts, err = aghnet.NewHostsContainer(osutil.RootDirFS(), hostsWatcher, paths...)
if err != nil {
closeErr := hostsWatcher.Close()
if errors.Is(err, aghnet.ErrNoHostsPaths) {
@@ -271,14 +263,18 @@ func setupOpts(opts options) (err error) {
}
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile
globalContext.pidFileName = opts.pidFile
}
return nil
}
// initContextClients initializes Context clients and related fields.
func initContextClients(ctx context.Context, logger *slog.Logger) (err error) {
func initContextClients(
ctx context.Context,
logger *slog.Logger,
sigHdlr *signalHandler,
) (err error) {
err = setupDNSFilteringConf(ctx, logger, config.Filtering)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
@@ -286,13 +282,13 @@ func initContextClients(ctx context.Context, logger *slog.Logger) (err error) {
}
//lint:ignore SA1019 Migration is not over.
config.DHCP.WorkDir = Context.workDir
config.DHCP.DataDir = Context.getDataDir()
config.DHCP.WorkDir = globalContext.workDir
config.DHCP.DataDir = globalContext.getDataDir()
config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified
Context.dhcpServer, err = dhcpd.Create(config.DHCP)
if Context.dhcpServer == nil || err != nil {
globalContext.dhcpServer, err = dhcpd.Create(config.DHCP)
if globalContext.dhcpServer == nil || err != nil {
// TODO(a.garipov): There are a lot of places in the code right
// now which assume that the DHCP server can be nil despite this
// condition. Inspect them and perhaps rewrite them to use
@@ -305,14 +301,15 @@ func initContextClients(ctx context.Context, logger *slog.Logger) (err error) {
arpDB = arpdb.New(logger.With(slogutil.KeyError, "arpdb"))
}
return Context.clients.Init(
return globalContext.clients.Init(
ctx,
logger,
config.Clients.Persistent,
Context.dhcpServer,
Context.etcHosts,
globalContext.dhcpServer,
globalContext.etcHosts,
arpDB,
config.Filtering,
sigHdlr,
)
}
@@ -374,15 +371,15 @@ func setupDNSFilteringConf(
pcTXTSuffix = `pc.dns.adguard.com.`
)
conf.EtcHosts = Context.etcHosts
conf.EtcHosts = globalContext.etcHosts
// TODO(s.chzhen): Use empty interface.
if Context.etcHosts == nil || !config.DNS.HostsFileEnabled {
if globalContext.etcHosts == nil || !config.DNS.HostsFileEnabled {
conf.EtcHosts = nil
}
conf.ConfigModified = onConfigModified
conf.HTTPRegister = httpRegister
conf.DataDir = Context.getDataDir()
conf.DataDir = globalContext.getDataDir()
conf.Filters = slices.Clone(config.Filters)
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
conf.UserRules = slices.Clone(config.UserRules)
@@ -560,7 +557,7 @@ func initWeb(
ReadHeaderTimeout: readHdrTimeout,
WriteTimeout: writeTimeout,
firstRun: Context.firstRun,
firstRun: globalContext.firstRun,
disableUpdate: disableUpdate,
runningAsService: opts.runningAsService,
serveHTTP3: config.DNS.ServeHTTP3,
@@ -583,7 +580,7 @@ func fatalOnError(err error) {
// run configures and starts AdGuard Home.
//
// TODO(e.burkov): Make opts a pointer.
func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalHandler) {
// Configure working dir.
err := initWorkingDir(opts)
fatalOnError(err)
@@ -599,10 +596,11 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
// TODO(a.garipov): Use slog everywhere.
slogLogger := newSlogLogger(ls)
sigHdlr.swapLogger(slogLogger)
// Print the first message after logger is configured.
log.Info(version.Full())
log.Debug("current working directory is %s", Context.workDir)
log.Debug("current working directory is %s", globalContext.workDir)
if opts.runningAsService {
log.Info("AdGuard Home is running as a service")
}
@@ -621,7 +619,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
// TODO(s.chzhen): Use it for the entire initialization process.
ctx := context.Background()
err = initContextClients(ctx, slogLogger)
err = initContextClients(ctx, slogLogger, sigHdlr)
fatalOnError(err)
err = setupOpts(opts)
@@ -632,13 +630,13 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
confPath := configFilePath()
upd, customURL := newUpdater(ctx, slogLogger, Context.workDir, confPath, execPath, config)
upd, customURL := newUpdater(ctx, slogLogger, globalContext.workDir, confPath, execPath, config)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(ctx, slogLogger, opts, upd)
if !Context.firstRun {
if !globalContext.firstRun {
// Save the updated config.
err = config.write()
fatalOnError(err)
@@ -648,33 +646,35 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
}
}
dataDir := Context.getDataDir()
dataDir := globalContext.getDataDir()
err = os.MkdirAll(dataDir, aghos.DefaultPermDir)
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDir))
GLMode = opts.glinetMode
// Init auth module.
Context.auth, err = initUsers()
globalContext.auth, err = initUsers()
fatalOnError(err)
Context.tls, err = newTLSManager(config.TLS, config.DNS.ServePlainDNS)
globalContext.tls, err = newTLSManager(config.TLS, config.DNS.ServePlainDNS)
if err != nil {
log.Error("initializing tls: %s", err)
onConfigModified()
}
Context.web, err = initWeb(ctx, opts, clientBuildFS, upd, slogLogger, customURL)
sigHdlr.addTLSManager(globalContext.tls)
globalContext.web, err = initWeb(ctx, opts, clientBuildFS, upd, slogLogger, customURL)
fatalOnError(err)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&globalContext, config)
fatalOnError(err)
if !Context.firstRun {
if !globalContext.firstRun {
err = initDNS(slogLogger, statsDir, querylogDir)
fatalOnError(err)
Context.tls.start()
globalContext.tls.start()
go func() {
startErr := startDNSServer()
@@ -684,8 +684,8 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
}
}()
if Context.dhcpServer != nil {
err = Context.dhcpServer.Start()
if globalContext.dhcpServer != nil {
err = globalContext.dhcpServer.Start()
if err != nil {
log.Error("starting dhcp server: %s", err)
}
@@ -693,10 +693,10 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
}
if !opts.noPermCheck {
checkPermissions(ctx, slogLogger, Context.workDir, confPath, dataDir, statsDir, querylogDir)
checkPermissions(ctx, slogLogger, globalContext.workDir, confPath, dataDir, statsDir, querylogDir)
}
Context.web.start(ctx)
globalContext.web.start(ctx)
// Wait for other goroutines to complete their job.
<-done
@@ -775,7 +775,7 @@ func checkPermissions(
// initUsers initializes context auth module. Clears config users field.
func initUsers() (auth *Auth, err error) {
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db")
var rateLimiter *authRateLimiter
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
@@ -810,7 +810,7 @@ func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
// startMods initializes and starts the DNS server after installation.
// baseLogger must not be nil.
func startMods(baseLogger *slog.Logger) (err error) {
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&globalContext, config)
if err != nil {
return err
}
@@ -820,7 +820,7 @@ func startMods(baseLogger *slog.Logger) (err error) {
return err
}
Context.tls.start()
globalContext.tls.start()
err = startDNSServer()
if err != nil {
@@ -883,14 +883,14 @@ func writePIDFile(fn string) bool {
func initConfigFilename(opts options) {
confPath := opts.confFilename
if confPath == "" {
Context.confFilePath = filepath.Join(Context.workDir, "AdGuardHome.yaml")
globalContext.confFilePath = filepath.Join(globalContext.workDir, "AdGuardHome.yaml")
return
}
log.Debug("config path overridden to %q from cmdline", confPath)
Context.confFilePath = confPath
globalContext.confFilePath = confPath
}
// initWorkingDir initializes the workDir. If no command-line arguments are
@@ -904,18 +904,18 @@ func initWorkingDir(opts options) (err error) {
if opts.workDir != "" {
// If there is a custom config file, use it's directory as our working dir
Context.workDir = opts.workDir
globalContext.workDir = opts.workDir
} else {
Context.workDir = filepath.Dir(execPath)
globalContext.workDir = filepath.Dir(execPath)
}
workDir, err := filepath.EvalSymlinks(Context.workDir)
workDir, err := filepath.EvalSymlinks(globalContext.workDir)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
Context.workDir = workDir
globalContext.workDir = workDir
return nil
}
@@ -924,13 +924,13 @@ func initWorkingDir(opts options) (err error) {
func cleanup(ctx context.Context) {
log.Info("stopping AdGuard Home")
if Context.web != nil {
Context.web.close(ctx)
Context.web = nil
if globalContext.web != nil {
globalContext.web.close(ctx)
globalContext.web = nil
}
if Context.auth != nil {
Context.auth.Close()
Context.auth = nil
if globalContext.auth != nil {
globalContext.auth.Close()
globalContext.auth = nil
}
err := stopDNSServer()
@@ -938,28 +938,28 @@ func cleanup(ctx context.Context) {
log.Error("stopping dns server: %s", err)
}
if Context.dhcpServer != nil {
err = Context.dhcpServer.Stop()
if globalContext.dhcpServer != nil {
err = globalContext.dhcpServer.Stop()
if err != nil {
log.Error("stopping dhcp server: %s", err)
}
}
if Context.etcHosts != nil {
if err = Context.etcHosts.Close(); err != nil {
if globalContext.etcHosts != nil {
if err = globalContext.etcHosts.Close(); err != nil {
log.Error("closing hosts container: %s", err)
}
}
if Context.tls != nil {
Context.tls = nil
if globalContext.tls != nil {
globalContext.tls = nil
}
}
// This function is called before application exits
func cleanupAlways() {
if len(Context.pidFileName) != 0 {
_ = os.Remove(Context.pidFileName)
if len(globalContext.pidFileName) != 0 {
_ = os.Remove(globalContext.pidFileName)
}
log.Info("stopped")
@@ -1007,8 +1007,8 @@ func printWebAddrs(proto, addr string, port uint16) {
// admin interface. proto is either schemeHTTP or schemeHTTPS.
func printHTTPAddresses(proto string) {
tlsConf := tlsConfigSettings{}
if Context.tls != nil {
Context.tls.WriteDiskConfig(&tlsConf)
if globalContext.tls != nil {
globalContext.tls.WriteDiskConfig(&tlsConf)
}
port := config.HTTPConfig.Address.Port()
@@ -1050,9 +1050,9 @@ func printHTTPAddresses(proto string) {
// detectFirstRun returns true if this is the first run of AdGuard Home.
func detectFirstRun() (ok bool) {
confPath := Context.confFilePath
confPath := globalContext.confFilePath
if !filepath.IsAbs(confPath) {
confPath = filepath.Join(Context.workDir, Context.confFilePath)
confPath = filepath.Join(globalContext.workDir, globalContext.confFilePath)
}
_, err := os.Stat(confPath)
@@ -1105,7 +1105,7 @@ func cmdlineUpdate(ctx context.Context, l *slog.Logger, opts options, upd *updat
os.Exit(osutil.ExitCodeSuccess)
}
err = upd.Update(Context.firstRun)
err = upd.Update(globalContext.firstRun)
fatalOnError(err)
err = restartService()

View File

@@ -17,7 +17,7 @@ func httpClient() (c *http.Client) {
// Do not use Context.dnsServer.DialContext directly in the struct literal
// below, since Context.dnsServer may be nil when this function is called.
dialContext := func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
return Context.dnsServer.DialContext(ctx, network, addr)
return globalContext.dnsServer.DialContext(ctx, network, addr)
}
return &http.Client{
@@ -27,8 +27,8 @@ func httpClient() (c *http.Client) {
DialContext: dialContext,
Proxy: httpProxy,
TLSClientConfig: &tls.Config{
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
RootCAs: globalContext.tlsRoots,
CipherSuites: globalContext.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
},

View File

@@ -66,7 +66,7 @@ func configureLogger(ls *logSettings) (err error) {
logFilePath := ls.File
if !filepath.IsAbs(logFilePath) {
logFilePath = filepath.Join(Context.workDir, logFilePath)
logFilePath = filepath.Join(globalContext.workDir, logFilePath)
}
log.SetOutput(&lumberjack.Logger{

View File

@@ -19,10 +19,10 @@ func setupDNSIPs(t testing.TB) {
t.Helper()
prevConfig := config
prevTLS := Context.tls
prevTLS := globalContext.tls
t.Cleanup(func() {
config = prevConfig
Context.tls = prevTLS
globalContext.tls = prevTLS
})
config = &configuration{
@@ -32,7 +32,7 @@ func setupDNSIPs(t testing.TB) {
},
}
Context.tls = &tlsManager{}
globalContext.tls = &tlsManager{}
}
func TestHandleMobileConfigDoH(t *testing.T) {
@@ -62,10 +62,10 @@ func TestHandleMobileConfigDoH(t *testing.T) {
})
t.Run("error_no_host", func(t *testing.T) {
oldTLSConf := Context.tls
t.Cleanup(func() { Context.tls = oldTLSConf })
oldTLSConf := globalContext.tls
t.Cleanup(func() { globalContext.tls = oldTLSConf })
Context.tls = &tlsManager{conf: tlsConfigSettings{}}
globalContext.tls = &tlsManager{conf: tlsConfigSettings{}}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil)
require.NoError(t, err)
@@ -134,10 +134,10 @@ func TestHandleMobileConfigDoT(t *testing.T) {
})
t.Run("error_no_host", func(t *testing.T) {
oldTLSConf := Context.tls
t.Cleanup(func() { Context.tls = oldTLSConf })
oldTLSConf := globalContext.tls
t.Cleanup(func() { globalContext.tls = oldTLSConf })
Context.tls = &tlsManager{conf: tlsConfigSettings{}}
globalContext.tls = &tlsManager{conf: tlsConfigSettings{}}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
require.NoError(t, err)

View File

@@ -47,7 +47,7 @@ type profileJSON struct {
// handleGetProfile is the handler for GET /control/profile endpoint.
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
u := Context.auth.getCurrentUser(r)
u := globalContext.auth.getCurrentUser(r)
var resp profileJSON
func() {

View File

@@ -36,6 +36,7 @@ type program struct {
signals chan os.Signal
done chan struct{}
opts options
sigHdlr *signalHandler
}
// type check
@@ -47,7 +48,7 @@ func (p *program) Start(_ service.Service) (err error) {
args := p.opts
args.runningAsService = true
go run(args, p.clientBuildFS, p.done)
go run(args, p.clientBuildFS, p.done, p.sigHdlr)
return nil
}
@@ -204,6 +205,7 @@ func handleServiceControlAction(
clientBuildFS fs.FS,
signals chan os.Signal,
done chan struct{},
sigHdlr *signalHandler,
) {
// Call chooseSystem explicitly to introduce OpenBSD support for service
// package. It's a noop for other GOOS values.
@@ -244,6 +246,7 @@ func handleServiceControlAction(
signals: signals,
done: done,
opts: runOpts,
sigHdlr: sigHdlr,
}, svcConfig)
if err != nil {
log.Fatalf("service: initializing service: %s", err)

121
internal/home/signal.go Normal file
View File

@@ -0,0 +1,121 @@
package home
import (
"context"
"log/slog"
"os"
"sync"
"sync/atomic"
"syscall"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/osutil"
)
// signalHandler processes incoming signals. It reloads configurations of
// stored entities on SIGHUP and performs cleanup on all other signals.
type signalHandler struct {
// logger is used to log the operation of the signal handler. Initially,
// [slog.Default] is used, but it should be swapped later using
// [signalHandler.swapLogger].
logger *atomic.Pointer[slog.Logger]
// mu protects clientStorage and tlsManager.
mu *sync.Mutex
// clientStorage is used to reload information about runtime clients with an
// ARP source.
clientStorage *client.Storage
// tlsManager is used to reload the TLS configuration.
tlsManager *tlsManager
// signals receives incoming signals.
signals <-chan os.Signal
// cleanup is called to perform cleanup on all incoming signals, except
// SIGHUP.
cleanup func(ctx context.Context)
}
// newSignalHandler returns a new properly initialized *signalHandler.
func newSignalHandler(
signals <-chan os.Signal,
cleanup func(ctx context.Context),
) (h *signalHandler) {
h = &signalHandler{
logger: &atomic.Pointer[slog.Logger]{},
mu: &sync.Mutex{},
signals: signals,
cleanup: cleanup,
}
h.logger.Store(slog.Default())
return h
}
// swapLogger replaces the stored logger with the given logger.
func (h *signalHandler) swapLogger(logger *slog.Logger) {
h.logger.Swap(logger)
}
// addClientStorage stores the client storage.
func (h *signalHandler) addClientStorage(s *client.Storage) {
h.mu.Lock()
defer h.mu.Unlock()
h.clientStorage = s
}
// addTLSManager stores the TLS manager.
func (h *signalHandler) addTLSManager(m *tlsManager) {
h.mu.Lock()
defer h.mu.Unlock()
h.tlsManager = m
}
// handle processes incoming signals. It blocks until a signal is received. It
// reloads configurations of stored entities on SIGHUP, or performs cleanup on
// all other signals. It is intended to be used as a goroutine.
func (h *signalHandler) handle(ctx context.Context) {
// NOTE: Avoid using [slogutil.RecoverAndExit] to prevent immediate
// evaluation of the logger.
defer func() {
v := recover()
if v == nil {
return
}
slogutil.PrintRecovered(ctx, h.logger.Load(), v)
os.Exit(osutil.ExitCodeFailure)
}()
for {
sig := <-h.signals
h.logger.Load().InfoContext(ctx, "received signal", "signal", sig)
switch sig {
case syscall.SIGHUP:
h.reloadConfig(ctx)
default:
h.cleanup(ctx)
}
}
}
// reloadConfig refreshes configurations of stored entities.
func (h *signalHandler) reloadConfig(ctx context.Context) {
h.mu.Lock()
defer h.mu.Unlock()
if h.clientStorage != nil {
h.clientStorage.ReloadARP(ctx)
}
if h.tlsManager != nil {
h.tlsManager.reload()
}
}

View File

@@ -112,7 +112,7 @@ func (m *tlsManager) start() {
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
Context.web.tlsConfigChanged(context.Background(), tlsConf)
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
}
// reload updates the configuration and restarts t.
@@ -160,7 +160,7 @@ func (m *tlsManager) reload() {
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
Context.web.tlsConfigChanged(context.Background(), tlsConf)
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
}
// loadTLSConf loads and validates the TLS configuration. The returned error is
@@ -463,7 +463,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
// same reason.
if restartHTTPS {
go func() {
Context.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
globalContext.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
}()
}
}
@@ -539,7 +539,7 @@ func validateCertChain(certs []*x509.Certificate, srvName string) (err error) {
opts := x509.VerifyOptions{
DNSName: srvName,
Roots: Context.tlsRoots,
Roots: globalContext.tlsRoots,
Intermediates: pool,
}
_, err = main.Verify(opts)

View File

@@ -129,7 +129,7 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
clientFS := http.FileServer(http.FS(conf.clientFS))
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
Context.mux.Handle("/", withMiddlewares(clientFS, gziphandler.GzipHandler, optionalAuthHandler, postInstallHandler))
globalContext.mux.Handle("/", withMiddlewares(clientFS, gziphandler.GzipHandler, optionalAuthHandler, postInstallHandler))
// add handlers for /install paths, we only need them when we're not configured yet
if conf.firstRun {
@@ -138,7 +138,7 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
"This is the first launch of AdGuard Home, redirecting everything to /install.html",
)
Context.mux.Handle("/install.html", preInstallHandler(clientFS))
globalContext.mux.Handle("/install.html", preInstallHandler(clientFS))
w.registerInstallHandlers()
} else {
registerControlHandlers(w)
@@ -154,7 +154,7 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
//
// TODO(a.garipov): Adapt for HTTP/3.
func webCheckPortAvailable(port uint16) (ok bool) {
if Context.web.httpsServer.server != nil {
if globalContext.web.httpsServer.server != nil {
return true
}
@@ -224,10 +224,14 @@ func (web *webAPI) start(ctx context.Context) {
errs := make(chan error, 2)
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitRequestBody), &http2.Server{})
hdlr := h2c.NewHandler(withMiddlewares(globalContext.mux, limitRequestBody), &http2.Server{})
logger := web.baseLogger.With(loggerKeyServer, "plain")
// TODO(a.garipov): Remove other logs like this in other code.
logMw := httputil.NewLogMiddleware(logger, slog.LevelDebug)
hdlr = logMw.Wrap(hdlr)
// Create a new instance, because the Web is not usable after Shutdown.
web.httpServer = &http.Server{
Addr: web.conf.BindAddr.String(),
@@ -238,7 +242,9 @@ func (web *webAPI) start(ctx context.Context) {
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
}
go func() {
defer slogutil.RecoverAndLog(ctx, web.logger)
defer slogutil.RecoverAndLog(ctx, logger)
logger.InfoContext(ctx, "starting plain server", "addr", web.httpServer.Addr)
errs <- web.httpServer.ListenAndServe()
}()
@@ -305,13 +311,17 @@ func (web *webAPI) tlsServerLoop(ctx context.Context) {
addr := netip.AddrPortFrom(web.conf.BindAddr.Addr(), portHTTPS).String()
logger := web.baseLogger.With(loggerKeyServer, "https")
// TODO(a.garipov): Remove other logs like this in other code.
logMw := httputil.NewLogMiddleware(logger, slog.LevelDebug)
hdlr := logMw.Wrap(withMiddlewares(globalContext.mux, limitRequestBody))
web.httpsServer.server = &http.Server{
Addr: addr,
Handler: withMiddlewares(Context.mux, limitRequestBody),
Handler: hdlr,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
RootCAs: globalContext.tlsRoots,
CipherSuites: globalContext.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
ReadTimeout: web.conf.ReadTimeout,
@@ -326,7 +336,7 @@ func (web *webAPI) tlsServerLoop(ctx context.Context) {
go web.mustStartHTTP3(ctx, addr)
}
web.logger.DebugContext(ctx, "starting https server")
logger.InfoContext(ctx, "starting https server")
err := web.httpsServer.server.ListenAndServeTLS("", "")
if !errors.Is(err, http.ErrServerClosed) {
cleanupAlways()
@@ -344,11 +354,11 @@ func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: Context.tlsRoots,
CipherSuites: Context.tlsCipherIDs,
RootCAs: globalContext.tlsRoots,
CipherSuites: globalContext.tlsCipherIDs,
MinVersion: tls.VersionTLS12,
},
Handler: withMiddlewares(Context.mux, limitRequestBody),
Handler: withMiddlewares(globalContext.mux, limitRequestBody),
}
web.logger.DebugContext(ctx, "starting http/3 server")

View File

@@ -20,6 +20,7 @@ import (
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"go.etcd.io/bbolt"
bbolterrors "go.etcd.io/bbolt/errors"
)
// checkInterval returns true if days is valid to be used as statistics
@@ -469,7 +470,7 @@ func (s *StatsCtx) flushDB(id, limit uint32, ptr *unit) (cont bool, sleepFor tim
// TODO(e.burkov): Improve the algorithm of deleting the oldest bucket
// to avoid the error.
lvl := slog.LevelDebug
if !errors.Is(delErr, bbolt.ErrBucketNotFound) {
if !errors.Is(delErr, bbolterrors.ErrBucketNotFound) {
isCommitable = false
lvl = slog.LevelError
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
@@ -78,15 +79,19 @@ func TestStats(t *testing.T) {
Client: cliIPStr,
Result: stats.RFiltered,
ProcessingTime: time.Microsecond * 123456,
Upstream: respUpstream,
UpstreamTime: time.Microsecond * 222222,
UpstreamStats: []*proxy.UpstreamStatistics{{
Address: respUpstream,
QueryDuration: time.Microsecond * 222222,
}},
}, {
Domain: reqDomain,
Client: cliIPStr,
Result: stats.RNotFiltered,
ProcessingTime: time.Microsecond * 123456,
Upstream: respUpstream,
UpstreamTime: time.Microsecond * 222222,
UpstreamStats: []*proxy.UpstreamStatistics{{
Address: respUpstream,
QueryDuration: time.Microsecond * 222222,
}},
}}
wantData := &stats.StatsResp{

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"go.etcd.io/bbolt"
@@ -62,8 +63,9 @@ type Entry struct {
// Domain is the domain name requested.
Domain string
// Upstream is the upstream DNS server.
Upstream string
// UpstreamStats contains the DNS query statistics for both the upstream and
// fallback DNS servers.
UpstreamStats []*proxy.UpstreamStatistics
// Result is the result of processing the request.
Result Result
@@ -71,9 +73,6 @@ type Entry struct {
// ProcessingTime is the duration of the request processing from the start
// of the request including timeouts.
ProcessingTime time.Duration
// UpstreamTime is the duration of the successful request to the upstream.
UpstreamTime time.Duration
}
// validate returns an error if entry is not valid.
@@ -329,10 +328,14 @@ func (u *unit) add(e *Entry) {
u.timeSum += pt
u.nTotal++
if e.Upstream != "" {
u.upstreamsResponses[e.Upstream]++
ut := uint64(e.UpstreamTime.Microseconds())
u.upstreamsTimeSum[e.Upstream] += ut
for _, s := range e.UpstreamStats {
if s.IsCached || s.Error != nil {
continue
}
addr := s.Address
u.upstreamsResponses[addr]++
u.upstreamsTimeSum[addr] += uint64(s.QueryDuration.Microseconds())
}
}

View File

@@ -1,6 +1,6 @@
module github.com/AdguardTeam/AdGuardHome/internal/tools
go 1.23.4
go 1.23.6
require (
github.com/fzipp/gocyclo v0.6.0
@@ -8,23 +8,23 @@ require (
github.com/gordonklaus/ineffassign v0.1.0
github.com/jstemmer/go-junit-report/v2 v2.1.0
github.com/kisielk/errcheck v1.8.0
github.com/securego/gosec/v2 v2.21.4
github.com/securego/gosec/v2 v2.22.1
github.com/uudashr/gocognit v1.2.0
golang.org/x/tools v0.28.0
golang.org/x/vuln v1.1.3
honnef.co/go/tools v0.5.1
golang.org/x/tools v0.30.0
golang.org/x/vuln v1.1.4
honnef.co/go/tools v0.6.0
mvdan.cc/gofumpt v0.7.0
mvdan.cc/sh/v3 v3.10.0
mvdan.cc/unparam v0.0.0-20240917084806-57a3b4290ba3
mvdan.cc/unparam v0.0.0-20250211232406-0e51248738fc
)
require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/ai v0.9.0 // indirect
cloud.google.com/go/auth v0.12.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/longrunning v0.6.3 // indirect
cloud.google.com/go v0.118.2 // indirect
cloud.google.com/go/ai v0.10.0 // indirect
cloud.google.com/go/auth v0.14.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/longrunning v0.6.4 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -33,35 +33,36 @@ require (
github.com/google/generative-ai-go v0.19.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/telemetry v0.0.0-20241204182053-c0ac0e154df3 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/api v0.211.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.68.1 // indirect
google.golang.org/protobuf v1.35.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect
golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/telemetry v0.0.0-20250214215356-6f9b61db478c // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/api v0.221.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
mvdan.cc/editorconfig v0.3.0 // indirect
)

View File

@@ -1,15 +1,15 @@
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/ai v0.9.0 h1:r1Ig8O8+Qr3Ia3WfoO+gokD0fxB2Rk4quppuKjmGMsY=
cloud.google.com/go/ai v0.9.0/go.mod h1:28bKM/oxmRgxmRgI1GLumFv+NSkt+DscAg/gF+54zzY=
cloud.google.com/go/auth v0.12.1 h1:n2Bj25BUMM0nvE9D2XLTiImanwZhO3DkfWSYS/SAJP4=
cloud.google.com/go/auth v0.12.1/go.mod h1:BFMu+TNpF3DmvfBO9ClqTR/SiqVIm7LukKF9mbendF4=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng=
cloud.google.com/go/longrunning v0.6.3/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
cloud.google.com/go v0.118.2 h1:bKXO7RXMFDkniAAvvuMrAPtQ/VHrs9e7J5UT3yrGdTY=
cloud.google.com/go v0.118.2/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M=
cloud.google.com/go/ai v0.10.0 h1:hwj6CI6sMKubXodoJJGTy/c2T1RbbLGM6TL3QoAvzU8=
cloud.google.com/go/ai v0.10.0/go.mod h1:kvnt2KeHqX8+41PVeMRBETDyQAp/RFvBWGdx/aGjNMo=
cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg=
cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
@@ -40,20 +40,20 @@ github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA=
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
@@ -66,106 +66,113 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/securego/gosec/v2 v2.21.4 h1:Le8MSj0PDmOnHJgUATjD96PaXRvCpKC+DGJvwyy0Mlk=
github.com/securego/gosec/v2 v2.21.4/go.mod h1:Jtb/MwRQfRxCXyCm1rfM1BEiiiTfUOdyzzAhlr6lUTA=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/securego/gosec/v2 v2.22.1 h1:IcBt3TpI5Y9VN1YlwjSpM2cHu0i3Iw52QM+PQeg7jN8=
github.com/securego/gosec/v2 v2.22.1/go.mod h1:4bb95X4Jz7VSEPdVjC0hD7C/yR6kdeUBvCPOy9gDQ0g=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 h1:1xaZTydL5Gsg78QharTwKfA9FY9CZ1VQj6D/AZEvHR0=
golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa h1:Br3+0EZZohShrmVVc85znGpxw7Ca8hsUJlrdT/JQGw8=
golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20241204182053-c0ac0e154df3 h1:rCLsPBq7l0E9Z451UgkWFkaWYhgt7dGmAlpD6hLjK5I=
golang.org/x/telemetry v0.0.0-20241204182053-c0ac0e154df3/go.mod h1:8h4Hgq+jcTvCDv2+i7NrfWwpYHcESleo2nGHxLbFLJ4=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20250214215356-6f9b61db478c h1:cA79rhMsZfyISI2vyH5j2XJb+QKQ7w/qWJNsR1PsRCI=
golang.org/x/telemetry v0.0.0-20250214215356-6f9b61db478c/go.mod h1:bDzXkYUaHzz51CtDy5kh/jR4lgPxsdbqC37kp/dzhCc=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/vuln v1.1.3 h1:NPGnvPOTgnjBc9HTaUx+nj+EaUYxl5SJOWqaDYGaFYw=
golang.org/x/vuln v1.1.3/go.mod h1:7Le6Fadm5FOqE9C926BCD0g12NWyhg7cxV4BwcPFuNY=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg=
google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU=
google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b h1:i+d0RZa8Hs2L/MuaOQYI+krthcxdEbEM2N+Tf3kJ4zk=
google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8=
honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
mvdan.cc/unparam v0.0.0-20240917084806-57a3b4290ba3 h1:YkmTN1n5U60NM02j7TCSWRlW3fqNiuXe/eVXf0dLFN8=
mvdan.cc/unparam v0.0.0-20240917084806-57a3b4290ba3/go.mod h1:z5yboO1sP1Q9pcfvS597TpfbNXQjphDlkCJHzt13ybc=
mvdan.cc/unparam v0.0.0-20250211232406-0e51248738fc h1:mEpjEutR7Qjdis+HqGQNdsJY/uRbH/MnyGXzLKMhDFo=
mvdan.cc/unparam v0.0.0-20250211232406-0e51248738fc/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE=