all: resync with master

This commit is contained in:
Ainar Garipov
2024-09-30 20:17:20 +03:00
parent c7d8b9ede1
commit 8cb5781770
153 changed files with 28633 additions and 27594 deletions

View File

@@ -1,94 +0,0 @@
package aghalg
// RingBuffer is the implementation of ring buffer data structure.
type RingBuffer[T any] struct {
buf []T
cur uint
full bool
}
// NewRingBuffer initializes the new instance of ring buffer. size must be
// greater or equal to zero.
func NewRingBuffer[T any](size uint) (rb *RingBuffer[T]) {
return &RingBuffer[T]{
buf: make([]T, size),
}
}
// Append appends an element to the buffer.
func (rb *RingBuffer[T]) Append(e T) {
if len(rb.buf) == 0 {
return
}
rb.buf[rb.cur] = e
rb.cur = (rb.cur + 1) % uint(cap(rb.buf))
if rb.cur == 0 {
rb.full = true
}
}
// Range calls cb for each element of the buffer. If cb returns false it stops.
func (rb *RingBuffer[T]) Range(cb func(T) (cont bool)) {
before, after := rb.splitCur()
for _, e := range before {
if !cb(e) {
return
}
}
for _, e := range after {
if !cb(e) {
return
}
}
}
// ReverseRange calls cb for each element of the buffer in reverse order. If
// cb returns false it stops.
func (rb *RingBuffer[T]) ReverseRange(cb func(T) (cont bool)) {
before, after := rb.splitCur()
for i := len(after) - 1; i >= 0; i-- {
if !cb(after[i]) {
return
}
}
for i := len(before) - 1; i >= 0; i-- {
if !cb(before[i]) {
return
}
}
}
// splitCur splits the buffer in two, before and after current position in
// chronological order. If buffer is not full, after is nil.
func (rb *RingBuffer[T]) splitCur() (before, after []T) {
if len(rb.buf) == 0 {
return nil, nil
}
cur := rb.cur
if !rb.full {
return rb.buf[:cur], nil
}
return rb.buf[cur:], rb.buf[:cur]
}
// Len returns a length of the buffer.
func (rb *RingBuffer[T]) Len() (l uint) {
if !rb.full {
return rb.cur
}
return uint(cap(rb.buf))
}
// Clear clears the buffer.
func (rb *RingBuffer[T]) Clear() {
rb.full = false
rb.cur = 0
}

View File

@@ -1,169 +0,0 @@
package aghalg_test
import (
"slices"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/stretchr/testify/assert"
)
// elements is a helper function that returns n elements of the buffer.
func elements(b *aghalg.RingBuffer[int], n uint, reverse bool) (es []int) {
fn := b.Range
if reverse {
fn = b.ReverseRange
}
var i uint
fn(func(e int) (cont bool) {
if i >= n {
return false
}
es = append(es, e)
i++
return true
})
return es
}
func TestNewRingBuffer(t *testing.T) {
t.Run("success_and_clear", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](5)
for i := range 10 {
b.Append(i)
}
assert.Equal(t, []int{5, 6, 7, 8, 9}, elements(b, b.Len(), false))
b.Clear()
assert.Zero(t, b.Len())
})
t.Run("zero", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](0)
for i := range 10 {
b.Append(i)
bufLen := b.Len()
assert.EqualValues(t, 0, bufLen)
assert.Empty(t, elements(b, bufLen, false))
assert.Empty(t, elements(b, bufLen, true))
}
})
t.Run("single", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](1)
for i := range 10 {
b.Append(i)
bufLen := b.Len()
assert.EqualValues(t, 1, bufLen)
assert.Equal(t, []int{i}, elements(b, bufLen, false))
assert.Equal(t, []int{i}, elements(b, bufLen, true))
}
})
}
func TestRingBuffer_Range(t *testing.T) {
const size = 5
b := aghalg.NewRingBuffer[int](size)
testCases := []struct {
name string
want []int
count int
length uint
}{{
name: "three",
count: 3,
length: 3,
want: []int{0, 1, 2},
}, {
name: "ten",
count: 10,
length: size,
want: []int{5, 6, 7, 8, 9},
}, {
name: "hundred",
count: 100,
length: size,
want: []int{95, 96, 97, 98, 99},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for i := range tc.count {
b.Append(i)
}
bufLen := b.Len()
assert.Equal(t, tc.length, bufLen)
want := tc.want
assert.Equal(t, want, elements(b, bufLen, false))
assert.Equal(t, want[:len(want)-1], elements(b, bufLen-1, false))
assert.Equal(t, want[:len(want)/2], elements(b, bufLen/2, false))
want = want[:cap(want)]
slices.Reverse(want)
assert.Equal(t, want, elements(b, bufLen, true))
assert.Equal(t, want[:len(want)-1], elements(b, bufLen-1, true))
assert.Equal(t, want[:len(want)/2], elements(b, bufLen/2, true))
})
}
}
func TestRingBuffer_Range_increment(t *testing.T) {
const size = 5
b := aghalg.NewRingBuffer[int](size)
testCases := []struct {
name string
want []int
}{{
name: "one",
want: []int{0},
}, {
name: "two",
want: []int{0, 1},
}, {
name: "three",
want: []int{0, 1, 2},
}, {
name: "four",
want: []int{0, 1, 2, 3},
}, {
name: "five",
want: []int{0, 1, 2, 3, 4},
}, {
name: "six",
want: []int{1, 2, 3, 4, 5},
}, {
name: "seven",
want: []int{2, 3, 4, 5, 6},
}, {
name: "eight",
want: []int{3, 4, 5, 6, 7},
}, {
name: "nine",
want: []int{4, 5, 6, 7, 8},
}, {
name: "ten",
want: []int{5, 6, 7, 8, 9},
}}
for i, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b.Append(i)
bufLen := b.Len()
assert.Equal(t, tc.want, elements(b, bufLen, false))
slices.Reverse(tc.want)
assert.Equal(t, tc.want, elements(b, bufLen, true))
})
}
}

View File

@@ -2,13 +2,16 @@
package aghhttp
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
// HTTP scheme constants.
@@ -31,12 +34,39 @@ func OK(w http.ResponseWriter) {
}
// Error writes formatted message to w and also logs it.
//
// TODO(s.chzhen): Remove it.
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
text := fmt.Sprintf(format, args...)
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
http.Error(w, text, code)
}
// ErrorAndLog writes formatted message to w and also logs it with the specified
// logging level.
func ErrorAndLog(
ctx context.Context,
l *slog.Logger,
r *http.Request,
w http.ResponseWriter,
code int,
format string,
args ...any,
) {
text := fmt.Sprintf(format, args...)
l.ErrorContext(
ctx,
"http error",
"host", r.Host,
"method", r.Method,
"raddr", r.RemoteAddr,
"request_uri", r.RequestURI,
slogutil.KeyError, text,
)
http.Error(w, text, code)
}
// UserAgent returns the ID of the service as a User-Agent string. It can also
// be used as the value of the Server HTTP header.
func UserAgent() (ua string) {

View File

@@ -6,10 +6,10 @@ import (
"bufio"
"fmt"
"io"
"net"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/netutil"
)
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
@@ -38,9 +38,13 @@ func (n interfaceName) rcConfStaticConfig(r io.Reader) (_ []string, cont bool, e
// TODO(e.burkov): Expand the check to cover possible
// configurations from man rc.conf(5).
fields := strings.Fields(line[cfgLeft:cfgRight])
if len(fields) >= 2 &&
strings.EqualFold(fields[0], "inet") &&
net.ParseIP(fields[1]) != nil {
switch {
case
len(fields) < 2,
!strings.EqualFold(fields[0], "inet"),
!netutil.IsValidIPString(fields[1]):
continue
default:
return nil, false, s.Err()
}
}

View File

@@ -6,10 +6,10 @@ import (
"bufio"
"fmt"
"io"
"net"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/netutil"
)
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
@@ -25,7 +25,13 @@ func hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) {
for s.Scan() {
line := strings.TrimSpace(s.Text())
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "inet" && net.ParseIP(fields[1]) != nil {
switch {
case
len(fields) < 2,
fields[0] != "inet",
!netutil.IsValidIPString(fields[1]):
continue
default:
return nil, false, s.Err()
}
}

View File

@@ -1,6 +1,6 @@
package aghos
// ConfigureSyslog reroutes standard logger output to syslog.
func ConfigureSyslog(serviceName string) error {
func ConfigureSyslog(serviceName string) (err error) {
return configureSyslog(serviceName)
}

View File

@@ -8,11 +8,15 @@ import (
"github.com/AdguardTeam/golibs/log"
)
func configureSyslog(serviceName string) error {
// configureSyslog sets standard log output to syslog.
func configureSyslog(serviceName string) (err error) {
w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
log.SetOutput(w)
return nil
}

View File

@@ -19,23 +19,30 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
return len(b), w.el.Info(1, string(b))
}
func configureSyslog(serviceName string) error {
// Note that the eventlog src is the same as the service name
// Otherwise, we will get "the description for event id cannot be found" warning in every log record
// configureSyslog sets standard log output to event log.
func configureSyslog(serviceName string) (err error) {
// Note that the eventlog src is the same as the service name, otherwise we
// will get "the description for event id cannot be found" warning in every
// log record.
// Continue if we receive "registry key already exists" or if we get
// ERROR_ACCESS_DENIED so that we can log without administrative permissions
// for pre-existing eventlog sources.
if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil {
if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED {
return err
}
err = eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error)
if err != nil &&
!strings.Contains(err.Error(), "registry key already exists") &&
err != windows.ERROR_ACCESS_DENIED {
// Don't wrap the error, because it's informative enough as is.
return err
}
el, err := eventlog.Open(serviceName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
log.SetOutput(&eventLogWriter{el: el})
return nil
}

View File

@@ -5,9 +5,10 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/netip"
"slices"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
)
// init makes sure that the cipher name map is filled.
@@ -75,15 +76,5 @@ func SaferCipherSuites() (safe []uint16) {
// CertificateHasIP returns true if cert has at least a single IP address among
// its subjectAltNames.
func CertificateHasIP(cert *x509.Certificate) (ok bool) {
if len(cert.IPAddresses) > 0 {
return true
}
for _, name := range cert.DNSNames {
if _, err := netip.ParseAddr(name); err == nil {
return true
}
}
return false
return len(cert.IPAddresses) > 0 || slices.ContainsFunc(cert.DNSNames, netutil.IsValidIPString)
}

View File

@@ -5,6 +5,7 @@ import (
"bufio"
"bytes"
"fmt"
"log/slog"
"net"
"net/netip"
"slices"
@@ -12,7 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil"
)
@@ -38,8 +39,8 @@ type Interface interface {
}
// New returns the [Interface] properly initialized for the OS.
func New() (arp Interface) {
return newARPDB()
func New(logger *slog.Logger) (arp Interface) {
return newARPDB(logger)
}
// Empty is the [Interface] implementation that does nothing.
@@ -69,6 +70,30 @@ type Neighbor struct {
MAC net.HardwareAddr
}
// newNeighbor returns the new initialized [Neighbor] by parsing string
// representations of IP and MAC addresses.
func newNeighbor(host, ipStr, macStr string) (n *Neighbor, err error) {
defer func() { err = errors.Annotate(err, "getting arp neighbor: %w") }()
ip, err := netip.ParseAddr(ipStr)
if err != nil {
// Don't wrap the error, as it will get annotated.
return nil, err
}
mac, err := net.ParseMAC(macStr)
if err != nil {
// Don't wrap the error, as it will get annotated.
return nil, err
}
return &Neighbor{
Name: host,
IP: ip,
MAC: mac,
}, nil
}
// Clone returns the deep copy of n.
func (n Neighbor) Clone() (clone Neighbor) {
return Neighbor{
@@ -80,10 +105,10 @@ func (n Neighbor) Clone() (clone Neighbor) {
// validatedHostname returns h if it's a valid hostname, or an empty string
// otherwise, logging the validation error.
func validatedHostname(h string) (host string) {
func validatedHostname(logger *slog.Logger, h string) (host string) {
err := netutil.ValidateHostname(h)
if err != nil {
log.Debug("arpdb: parsing arp output: host: %s", err)
logger.Debug("parsing host of arp output", slogutil.KeyError, err)
return ""
}
@@ -132,15 +157,18 @@ func (ns *neighs) reset(with []Neighbor) {
// parseNeighsFunc parses the text from sc as if it'd be an output of some
// ARP-related command. lenHint is a hint for the size of the allocated slice
// of Neighbors.
type parseNeighsFunc func(sc *bufio.Scanner, lenHint int) (ns []Neighbor)
//
// TODO(s.chzhen): Return []*Neighbor instead.
type parseNeighsFunc func(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor)
// cmdARPDB is the implementation of the [Interface] that uses command line to
// retrieve data.
type cmdARPDB struct {
parse parseNeighsFunc
ns *neighs
cmd string
args []string
logger *slog.Logger
parse parseNeighsFunc
ns *neighs
cmd string
args []string
}
// type check
@@ -158,7 +186,7 @@ func (arp *cmdARPDB) Refresh() (err error) {
}
sc := bufio.NewScanner(bytes.NewReader(out))
ns := arp.parse(sc, arp.ns.len())
ns := arp.parse(arp.logger, sc, arp.ns.len())
if err = sc.Err(); err != nil {
// TODO(e.burkov): This error seems unreachable. Investigate.
return fmt.Errorf("scanning the output: %w", err)

View File

@@ -4,17 +4,17 @@ package arpdb
import (
"bufio"
"net"
"net/netip"
"log/slog"
"strings"
"sync"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
func newARPDB() (arp *cmdARPDB) {
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
return &cmdARPDB{
parse: parseArpA,
logger: logger,
parse: parseArpA,
ns: &neighs{
mu: &sync.RWMutex{},
ns: make([]Neighbor, 0),
@@ -33,7 +33,7 @@ func newARPDB() (arp *cmdARPDB) {
// The expected input format:
//
// host.name (192.168.0.1) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint)
for sc.Scan() {
ln := sc.Text()
@@ -48,26 +48,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue
}
ip, err := netip.ParseAddr(ipStr[1 : len(ipStr)-1])
host := validatedHostname(logger, fields[0])
n, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])
if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err)
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue
}
hwStr := fields[3]
mac, err := net.ParseMAC(hwStr)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
Name: validatedHostname(fields[0]),
})
ns = append(ns, *n)
}
return ns

View File

@@ -11,6 +11,7 @@ import (
"testing"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -61,7 +62,7 @@ func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err
func Test_New(t *testing.T) {
var a Interface
require.NotPanics(t, func() { a = New() })
require.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })
assert.NotNil(t, a)
}
@@ -201,8 +202,9 @@ func Test_NewARPDBs(t *testing.T) {
func TestCmdARPDB_arpa(t *testing.T) {
a := &cmdARPDB{
cmd: "cmd",
parse: parseArpA,
logger: slogutil.NewDiscardLogger(),
cmd: "cmd",
parse: parseArpA,
ns: &neighs{
mu: &sync.RWMutex{},
ns: make([]Neighbor, 0),

View File

@@ -6,17 +6,18 @@ import (
"bufio"
"fmt"
"io/fs"
"log/slog"
"net"
"net/netip"
"strings"
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/stringutil"
)
func newARPDB() (arp *arpdbs) {
func newARPDB(logger *slog.Logger) (arp *arpdbs) {
// Use the common storage among the implementations.
ns := &neighs{
mu: &sync.RWMutex{},
@@ -39,9 +40,10 @@ func newARPDB() (arp *arpdbs) {
},
// Then, try "arp -a -n".
&cmdARPDB{
parse: parseF,
ns: ns,
cmd: "arp",
logger: logger,
parse: parseF,
ns: ns,
cmd: "arp",
// Use -n flag to avoid resolving the hostnames of the neighbors.
// By default ARP attempts to resolve the hostnames via DNS. See
// man 8 arp.
@@ -51,10 +53,11 @@ func newARPDB() (arp *arpdbs) {
},
// Finally, try "ip neigh".
&cmdARPDB{
parse: parseIPNeigh,
ns: ns,
cmd: "ip",
args: []string{"neigh"},
logger: logger,
parse: parseIPNeigh,
ns: ns,
cmd: "ip",
args: []string{"neigh"},
},
)
}
@@ -131,7 +134,7 @@ func (arp *fsysARPDB) Neighbors() (ns []Neighbor) {
//
// IP address HW type Flags HW address Mask Device
// 192.168.11.98 0x1 0x2 5a:92:df:a9:7e:28 * wan
func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
func parseArpAWrt(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
if !sc.Scan() {
// Skip the header.
return
@@ -146,25 +149,14 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue
}
ip, err := netip.ParseAddr(fields[0])
n, err := newNeighbor("", fields[0], fields[3])
if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err)
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue
}
hwStr := fields[3]
mac, err := net.ParseMAC(hwStr)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
})
ns = append(ns, *n)
}
return ns
@@ -174,7 +166,7 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
// expected input format:
//
// hostname (192.168.1.1) at ab:cd:ef:ab:cd:ef [ether] on enp0s3
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint)
for sc.Scan() {
ln := sc.Text()
@@ -189,26 +181,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue
}
ip, err := netip.ParseAddr(ipStr[1 : len(ipStr)-1])
host := validatedHostname(logger, fields[0])
n, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])
if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err)
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue
}
hwStr := fields[3]
mac, err := net.ParseMAC(hwStr)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
Name: validatedHostname(fields[0]),
})
ns = append(ns, *n)
}
return ns
@@ -218,7 +199,7 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
// expected input format:
//
// 192.168.1.1 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef REACHABLE
func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
func parseIPNeigh(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint)
for sc.Scan() {
ln := sc.Text()
@@ -228,27 +209,14 @@ func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue
}
n := Neighbor{}
ip, err := netip.ParseAddr(fields[0])
n, err := newNeighbor("", fields[0], fields[4])
if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err)
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue
} else {
n.IP = ip
}
mac, err := net.ParseMAC(fields[4])
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
} else {
n.MAC = mac
}
ns = append(ns, n)
ns = append(ns, *n)
}
return ns

View File

@@ -9,6 +9,7 @@ import (
"testing"
"testing/fstest"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -69,9 +70,10 @@ func TestCmdARPDB_linux(t *testing.T) {
t.Run("wrt", func(t *testing.T) {
a := &cmdARPDB{
parse: parseArpAWrt,
cmd: "arp",
args: []string{"-a"},
logger: slogutil.NewDiscardLogger(),
parse: parseArpAWrt,
cmd: "arp",
args: []string{"-a"},
ns: &neighs{
mu: &sync.RWMutex{},
ns: make([]Neighbor, 0),
@@ -86,9 +88,10 @@ func TestCmdARPDB_linux(t *testing.T) {
t.Run("ip_neigh", func(t *testing.T) {
a := &cmdARPDB{
parse: parseIPNeigh,
cmd: "ip",
args: []string{"neigh"},
logger: slogutil.NewDiscardLogger(),
parse: parseIPNeigh,
cmd: "ip",
args: []string{"neigh"},
ns: &neighs{
mu: &sync.RWMutex{},
ns: make([]Neighbor, 0),

View File

@@ -4,17 +4,17 @@ package arpdb
import (
"bufio"
"net"
"net/netip"
"log/slog"
"strings"
"sync"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
func newARPDB() (arp *cmdARPDB) {
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
return &cmdARPDB{
parse: parseArpA,
logger: logger,
parse: parseArpA,
ns: &neighs{
mu: &sync.RWMutex{},
ns: make([]Neighbor, 0),
@@ -34,7 +34,7 @@ func newARPDB() (arp *cmdARPDB) {
//
// Host Ethernet Address Netif Expire Flags
// 192.168.1.1 ab:cd:ef:ab:cd:ef em0 19m59s
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
// Skip the header.
if !sc.Scan() {
return nil
@@ -49,27 +49,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue
}
n := Neighbor{}
ip, err := netip.ParseAddr(fields[0])
n, err := newNeighbor("", fields[0], fields[1])
if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err)
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue
} else {
n.IP = ip
}
mac, err := net.ParseMAC(fields[1])
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
} else {
n.MAC = mac
}
ns = append(ns, n)
ns = append(ns, *n)
}
return ns

View File

@@ -4,17 +4,17 @@ package arpdb
import (
"bufio"
"net"
"net/netip"
"log/slog"
"strings"
"sync"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
func newARPDB() (arp *cmdARPDB) {
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
return &cmdARPDB{
parse: parseArpA,
logger: logger,
parse: parseArpA,
ns: &neighs{
mu: &sync.RWMutex{},
ns: make([]Neighbor, 0),
@@ -31,7 +31,7 @@ func newARPDB() (arp *cmdARPDB) {
// Internet Address Physical Address Type
// 192.168.56.1 0a-00-27-00-00-00 dynamic
// 192.168.56.255 ff-ff-ff-ff-ff-ff static
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint)
for sc.Scan() {
ln := sc.Text()
@@ -44,24 +44,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue
}
ip, err := netip.ParseAddr(fields[0])
n, err := newNeighbor("", fields[0], fields[1])
if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err)
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue
}
mac, err := net.ParseMAC(fields[1])
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
})
ns = append(ns, *n)
}
return ns

View File

@@ -2,6 +2,7 @@ package client
import (
"context"
"log/slog"
"net/netip"
"sync"
"time"
@@ -11,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
)
@@ -38,6 +40,10 @@ func (EmptyAddrProc) Close() (_ error) { return nil }
// DefaultAddrProcConfig is the configuration structure for address processors.
type DefaultAddrProcConfig struct {
// BaseLogger is used to create loggers with custom prefixes for sources of
// information about runtime clients. It must not be nil.
BaseLogger *slog.Logger
// DialContext is used to create TCP connections to WHOIS servers.
// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
DialContext aghnet.DialContextFunc
@@ -147,6 +153,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
if c.UseRDNS {
p.rdns = rdns.New(&rdns.Config{
Logger: c.BaseLogger.With(slogutil.KeyPrefix, "rdns"),
Exchanger: c.Exchanger,
CacheSize: defaultCacheSize,
CacheTTL: defaultIPTTL,
@@ -154,7 +161,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
}
if c.UseWHOIS {
p.whois = newWHOIS(c.DialContext)
p.whois = newWHOIS(c.BaseLogger.With(slogutil.KeyPrefix, "whois"), c.DialContext)
}
go p.process(c.CatchPanics)
@@ -168,7 +175,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
// newWHOIS returns a whois.Interface instance using the given function for
// dialing.
func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
func newWHOIS(logger *slog.Logger, dialFunc aghnet.DialContextFunc) (w whois.Interface) {
// TODO(s.chzhen): Consider making configurable.
const (
// defaultTimeout is the timeout for WHOIS requests.
@@ -186,6 +193,7 @@ func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
)
return whois.New(&whois.Config{
Logger: logger,
DialContext: dialFunc,
ServerAddr: whois.DefaultServer,
Port: whois.DefaultPort,
@@ -227,9 +235,11 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
log.Info("clients: processing addresses")
ctx := context.TODO()
for ip := range p.clientIPs {
host := p.processRDNS(ip)
info := p.processWHOIS(ip)
host := p.processRDNS(ctx, ip)
info := p.processWHOIS(ctx, ip)
p.addrUpdater.UpdateAddress(ip, host, info)
}
@@ -239,7 +249,7 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
// processRDNS resolves the clients' IP addresses using reverse DNS. host is
// empty if there were errors or if the information hasn't changed.
func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
func (p *DefaultAddrProc) processRDNS(ctx context.Context, ip netip.Addr) (host string) {
start := time.Now()
log.Debug("clients: processing %s with rdns", ip)
defer func() {
@@ -251,7 +261,7 @@ func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
return
}
host, changed := p.rdns.Process(ip)
host, changed := p.rdns.Process(ctx, ip)
if !changed {
host = ""
}
@@ -268,7 +278,7 @@ func (p *DefaultAddrProc) shouldResolve(ip netip.Addr) (ok bool) {
// processWHOIS looks up the information about clients' IP addresses in the
// WHOIS databases. info is nil if there were errors or if the information
// hasn't changed.
func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
func (p *DefaultAddrProc) processWHOIS(ctx context.Context, ip netip.Addr) (info *whois.Info) {
start := time.Now()
log.Debug("clients: processing %s with whois", ip)
defer func() {
@@ -277,7 +287,7 @@ func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
// TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the
// context.
info, changed := p.whois.Process(context.Background(), ip)
info, changed := p.whois.Process(ctx, ip)
if !changed {
info = nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/fakenet"
@@ -99,6 +100,7 @@ func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
updInfoCh := make(chan *whois.Info, 1)
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
BaseLogger: slogutil.NewDiscardLogger(),
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
panic("not implemented")
},
@@ -208,6 +210,7 @@ func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
updInfoCh := make(chan *whois.Info, 1)
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
BaseLogger: slogutil.NewDiscardLogger(),
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
return whoisConn, nil
},

View File

@@ -8,6 +8,7 @@ import (
"encoding"
"fmt"
"net/netip"
"slices"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
)
@@ -118,8 +119,9 @@ func (r *Runtime) Info() (cs Source, host string) {
return cs, info[0]
}
// SetInfo sets a host as a client information from the cs.
func (r *Runtime) SetInfo(cs Source, hosts []string) {
// setInfo sets a host as a client information from the cs.
func (r *Runtime) setInfo(cs Source, hosts []string) {
// TODO(s.chzhen): Use contract where hosts must contain non-empty host.
if len(hosts) == 1 && hosts[0] == "" {
hosts = []string{}
}
@@ -136,13 +138,13 @@ func (r *Runtime) SetInfo(cs Source, hosts []string) {
}
}
// WHOIS returns a WHOIS client information.
// WHOIS returns a copy of WHOIS client information.
func (r *Runtime) WHOIS() (info *whois.Info) {
return r.whois
return r.whois.Clone()
}
// SetWHOIS sets a WHOIS client information. info must be non-nil.
func (r *Runtime) SetWHOIS(info *whois.Info) {
// setWHOIS sets a WHOIS client information. info must be non-nil.
func (r *Runtime) setWHOIS(info *whois.Info) {
r.whois = info
}
@@ -175,3 +177,15 @@ func (r *Runtime) isEmpty() (ok bool) {
func (r *Runtime) Addr() (ip netip.Addr) {
return r.ip
}
// clone returns a deep copy of the runtime client.
func (r *Runtime) clone() (c *Runtime) {
return &Runtime{
ip: r.ip,
whois: r.whois.Clone(),
arp: slices.Clone(r.arp),
rdns: slices.Clone(r.rdns),
dhcp: slices.Clone(r.dhcp),
hostsFile: slices.Clone(r.hostsFile),
}
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
@@ -136,7 +135,8 @@ type Persistent struct {
}
// validate returns an error if persistent client information contains errors.
func (c *Persistent) validate(allTags *container.MapSet[string]) (err error) {
// allTags must be sorted.
func (c *Persistent) validate(allTags []string) (err error) {
switch {
case c.Name == "":
return errors.Error("empty name")
@@ -157,7 +157,8 @@ func (c *Persistent) validate(allTags *container.MapSet[string]) (err error) {
}
for _, t := range c.Tags {
if !allTags.Has(t) {
_, ok := slices.BinarySearch(allTags, t)
if !ok {
return fmt.Errorf("invalid tag: %q", t)
}
}

View File

@@ -1,11 +1,8 @@
package client
import (
"net/netip"
"testing"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -125,69 +122,3 @@ func TestPersistent_EqualIDs(t *testing.T) {
})
}
}
func TestPersistent_Validate(t *testing.T) {
const (
allowedTag = "allowed_tag"
notAllowedTag = "not_allowed_tag"
)
allowedTags := container.NewMapSet(allowedTag)
testCases := []struct {
name string
cli *Persistent
wantErrMsg string
}{{
name: "success",
cli: &Persistent{
Name: "basic",
IPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
UID: MustNewUID(),
},
wantErrMsg: "",
}, {
name: "empty_name",
cli: &Persistent{
Name: "",
},
wantErrMsg: "empty name",
}, {
name: "no_id",
cli: &Persistent{
Name: "no_id",
},
wantErrMsg: "id required",
}, {
name: "no_uid",
cli: &Persistent{
Name: "no_uid",
IPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
},
wantErrMsg: "uid required",
}, {
name: "not_allowed_tag",
cli: &Persistent{
Name: "basic",
IPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
UID: MustNewUID(),
Tags: []string{
notAllowedTag,
},
},
wantErrMsg: `invalid tag: "` + notAllowedTag + `"`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.cli.validate(allowedTags)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}

View File

@@ -2,39 +2,34 @@ package client
import "net/netip"
// RuntimeIndex stores information about runtime clients.
type RuntimeIndex struct {
// runtimeIndex stores information about runtime clients.
type runtimeIndex struct {
// index maps IP address to runtime client.
index map[netip.Addr]*Runtime
}
// NewRuntimeIndex returns initialized runtime index.
func NewRuntimeIndex() (ri *RuntimeIndex) {
return &RuntimeIndex{
// newRuntimeIndex returns initialized runtime index.
func newRuntimeIndex() (ri *runtimeIndex) {
return &runtimeIndex{
index: map[netip.Addr]*Runtime{},
}
}
// Client returns the saved runtime client by ip. If no such client exists,
// client returns the saved runtime client by ip. If no such client exists,
// returns nil.
func (ri *RuntimeIndex) Client(ip netip.Addr) (rc *Runtime) {
func (ri *runtimeIndex) client(ip netip.Addr) (rc *Runtime) {
return ri.index[ip]
}
// Add saves the runtime client in the index. IP address of a client must be
// add saves the runtime client in the index. IP address of a client must be
// unique. See [Runtime.Client]. rc must not be nil.
func (ri *RuntimeIndex) Add(rc *Runtime) {
func (ri *runtimeIndex) add(rc *Runtime) {
ip := rc.Addr()
ri.index[ip] = rc
}
// Size returns the number of the runtime clients.
func (ri *RuntimeIndex) Size() (n int) {
return len(ri.index)
}
// Range calls f for each runtime client in an undefined order.
func (ri *RuntimeIndex) Range(f func(rc *Runtime) (cont bool)) {
// rangeClients calls f for each runtime client in an undefined order.
func (ri *runtimeIndex) rangeClients(f func(rc *Runtime) (cont bool)) {
for _, rc := range ri.index {
if !f(rc) {
return
@@ -42,17 +37,31 @@ func (ri *RuntimeIndex) Range(f func(rc *Runtime) (cont bool)) {
}
}
// Delete removes the runtime client by ip.
func (ri *RuntimeIndex) Delete(ip netip.Addr) {
delete(ri.index, ip)
// setInfo sets the client information from cs for runtime client stored by ip.
// If no such client exists, it creates one.
func (ri *runtimeIndex) setInfo(ip netip.Addr, cs Source, hosts []string) (rc *Runtime) {
rc = ri.index[ip]
if rc == nil {
rc = NewRuntime(ip)
ri.add(rc)
}
rc.setInfo(cs, hosts)
return rc
}
// DeleteBySource removes all runtime clients that have information only from
// the specified source and returns the number of removed clients.
func (ri *RuntimeIndex) DeleteBySource(src Source) (n int) {
for ip, rc := range ri.index {
// clearSource removes information from the specified source from all clients.
func (ri *runtimeIndex) clearSource(src Source) {
for _, rc := range ri.index {
rc.unset(src)
}
}
// removeEmpty removes empty runtime clients and returns the number of removed
// clients.
func (ri *runtimeIndex) removeEmpty() (n int) {
for ip, rc := range ri.index {
if rc.isEmpty() {
delete(ri.index, ip)
n++

View File

@@ -1,85 +0,0 @@
package client_test
import (
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/stretchr/testify/assert"
)
func TestRuntimeIndex(t *testing.T) {
const cliSrc = client.SourceARP
var (
ip1 = netip.MustParseAddr("1.1.1.1")
ip2 = netip.MustParseAddr("2.2.2.2")
ip3 = netip.MustParseAddr("3.3.3.3")
)
ri := client.NewRuntimeIndex()
currentSize := 0
testCases := []struct {
ip netip.Addr
name string
hosts []string
src client.Source
}{{
src: cliSrc,
ip: ip1,
name: "1",
hosts: []string{"host1"},
}, {
src: cliSrc,
ip: ip2,
name: "2",
hosts: []string{"host2"},
}, {
src: cliSrc,
ip: ip3,
name: "3",
hosts: []string{"host3"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rc := client.NewRuntime(tc.ip)
rc.SetInfo(tc.src, tc.hosts)
ri.Add(rc)
currentSize++
got := ri.Client(tc.ip)
assert.Equal(t, rc, got)
})
}
t.Run("size", func(t *testing.T) {
assert.Equal(t, currentSize, ri.Size())
})
t.Run("range", func(t *testing.T) {
s := 0
ri.Range(func(rc *client.Runtime) (cont bool) {
s++
return true
})
assert.Equal(t, currentSize, s)
})
t.Run("delete", func(t *testing.T) {
ri.Delete(ip1)
currentSize--
assert.Equal(t, currentSize, ri.Size())
})
t.Run("delete_by_src", func(t *testing.T) {
assert.Equal(t, currentSize, ri.DeleteBySource(cliSrc))
assert.Equal(t, 0, ri.Size())
})
}

View File

@@ -1,29 +1,113 @@
package client
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"sync"
"time"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
)
// Config is the client storage configuration structure.
//
// TODO(s.chzhen): Expand.
type Config struct {
// AllowedTags is a list of all allowed client tags.
AllowedTags []string
// allowedTags is the list of available client tags.
var allowedTags = []string{
"device_audio",
"device_camera",
"device_gameconsole",
"device_laptop",
"device_nas", // Network-attached Storage
"device_other",
"device_pc",
"device_phone",
"device_printer",
"device_securityalarm",
"device_tablet",
"device_tv",
"os_android",
"os_ios",
"os_linux",
"os_macos",
"os_other",
"os_windows",
"user_admin",
"user_child",
"user_regular",
}
// DHCP is an interface for accessing DHCP lease data the [Storage] needs.
type DHCP interface {
// Leases returns all the DHCP leases.
Leases() (leases []*dhcpsvc.Lease)
// HostByIP returns the hostname of the DHCP client with the given IP
// address. host will be empty if there is no such client, due to an
// assumption that a DHCP client must always have a hostname.
HostByIP(ip netip.Addr) (host string)
// MACByIP returns the MAC address for the given IP address leased. It
// returns nil if there is no such client, due to an assumption that a DHCP
// client must always have a MAC address.
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
}
// EmptyDHCP is the empty [DHCP] implementation that does nothing.
type EmptyDHCP struct{}
// type check
var _ DHCP = EmptyDHCP{}
// Leases implements the [DHCP] interface for emptyDHCP.
func (EmptyDHCP) Leases() (leases []*dhcpsvc.Lease) { return nil }
// HostByIP implements the [DHCP] interface for emptyDHCP.
func (EmptyDHCP) HostByIP(_ netip.Addr) (host string) { return "" }
// MACByIP implements the [DHCP] interface for emptyDHCP.
func (EmptyDHCP) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil }
// HostsContainer is an interface for receiving updates to the system hosts
// file.
type HostsContainer interface {
Upd() (updates <-chan *hostsfile.DefaultStorage)
}
// StorageConfig is the client storage configuration structure.
type StorageConfig struct {
// DHCP is used to match IPs against MACs of persistent clients and update
// [SourceDHCP] runtime client information. It must not be nil.
DHCP DHCP
// EtcHosts is used to update [SourceHostsFile] runtime client information.
EtcHosts HostsContainer
// ARPDB is used to update [SourceARP] runtime client information.
ARPDB arpdb.Interface
// InitialClients is a list of persistent clients parsed from the
// configuration file. Each client must not be nil.
InitialClients []*Persistent
// ARPClientsUpdatePeriod defines how often [SourceARP] runtime client
// information is updated.
ARPClientsUpdatePeriod time.Duration
// RuntimeSourceDHCP specifies whether to update [SourceDHCP] information
// of runtime clients.
RuntimeSourceDHCP bool
}
// Storage contains information about persistent and runtime clients.
type Storage struct {
// allowedTags is a set of all allowed tags.
allowedTags *container.MapSet[string]
// mu protects indexes of persistent and runtime clients.
mu *sync.Mutex
@@ -31,21 +115,250 @@ type Storage struct {
index *index
// runtimeIndex contains information about runtime clients.
runtimeIndex *runtimeIndex
// dhcp is used to update [SourceDHCP] runtime client information.
dhcp DHCP
// etcHosts is used to update [SourceHostsFile] runtime client information.
etcHosts HostsContainer
// arpDB is used to update [SourceARP] runtime client information.
arpDB arpdb.Interface
// done is the shutdown signaling channel.
done chan struct{}
// allowedTags is a sorted list of all allowed tags. It must not be
// modified after initialization.
//
// TODO(s.chzhen): Use it.
runtimeIndex *RuntimeIndex
// TODO(s.chzhen): Use custom type.
allowedTags []string
// arpClientsUpdatePeriod defines how often [SourceARP] runtime client
// information is updated. It must be greater than zero.
arpClientsUpdatePeriod time.Duration
// runtimeSourceDHCP specifies whether to update [SourceDHCP] information
// of runtime clients.
runtimeSourceDHCP bool
}
// NewStorage returns initialized client storage. conf must not be nil.
func NewStorage(conf *Config) (s *Storage) {
allowedTags := container.NewMapSet(conf.AllowedTags...)
func NewStorage(conf *StorageConfig) (s *Storage, err error) {
tags := slices.Clone(allowedTags)
slices.Sort(tags)
return &Storage{
allowedTags: allowedTags,
mu: &sync.Mutex{},
index: newIndex(),
runtimeIndex: NewRuntimeIndex(),
s = &Storage{
allowedTags: tags,
mu: &sync.Mutex{},
index: newIndex(),
runtimeIndex: newRuntimeIndex(),
dhcp: conf.DHCP,
etcHosts: conf.EtcHosts,
arpDB: conf.ARPDB,
done: make(chan struct{}),
arpClientsUpdatePeriod: conf.ARPClientsUpdatePeriod,
runtimeSourceDHCP: conf.RuntimeSourceDHCP,
}
for i, p := range conf.InitialClients {
err = s.Add(p)
if err != nil {
return nil, fmt.Errorf("adding client %q at index %d: %w", p.Name, i, err)
}
}
s.ReloadARP()
return s, nil
}
// Start starts the goroutines for updating the runtime client information.
//
// TODO(s.chzhen): Pass context.
func (s *Storage) Start(_ context.Context) (err error) {
go s.periodicARPUpdate()
go s.handleHostsUpdates()
return nil
}
// Shutdown gracefully stops the client storage.
//
// TODO(s.chzhen): Pass context.
func (s *Storage) Shutdown(_ context.Context) (err error) {
close(s.done)
return s.closeUpstreams()
}
// periodicARPUpdate periodically reloads runtime clients from ARP. It is
// intended to be used as a goroutine.
func (s *Storage) periodicARPUpdate() {
defer log.OnPanic("storage")
t := time.NewTicker(s.arpClientsUpdatePeriod)
for {
select {
case <-t.C:
s.ReloadARP()
case <-s.done:
return
}
}
}
// ReloadARP reloads runtime clients from ARP, if configured.
func (s *Storage) ReloadARP() {
if s.arpDB != nil {
s.addFromSystemARP()
}
}
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
// command.
func (s *Storage) addFromSystemARP() {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.arpDB.Refresh(); err != nil {
s.arpDB = arpdb.Empty{}
log.Error("refreshing arp container: %s", err)
return
}
ns := s.arpDB.Neighbors()
if len(ns) == 0 {
log.Debug("refreshing arp container: the update is empty")
return
}
src := SourceARP
s.runtimeIndex.clearSource(src)
for _, n := range ns {
s.runtimeIndex.setInfo(n.IP, src, []string{n.Name})
}
removed := s.runtimeIndex.removeEmpty()
log.Debug("storage: added %d, removed %d client aliases from arp neighborhood", len(ns), removed)
}
// handleHostsUpdates receives the updates from the hosts container and adds
// them to the clients storage. It is intended to be used as a goroutine.
func (s *Storage) handleHostsUpdates() {
if s.etcHosts == nil {
return
}
defer log.OnPanic("storage")
for {
select {
case upd, ok := <-s.etcHosts.Upd():
if !ok {
return
}
s.addFromHostsFile(upd)
case <-s.done:
return
}
}
}
// addFromHostsFile fills the client-hostname pairing index from the system's
// hosts files.
func (s *Storage) addFromHostsFile(hosts *hostsfile.DefaultStorage) {
s.mu.Lock()
defer s.mu.Unlock()
src := SourceHostsFile
s.runtimeIndex.clearSource(src)
added := 0
hosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {
// Only the first name of the first record is considered a canonical
// hostname for the IP address.
//
// TODO(e.burkov): Consider using all the names from all the records.
s.runtimeIndex.setInfo(addr, src, []string{names[0]})
added++
return true
})
removed := s.runtimeIndex.removeEmpty()
log.Debug("storage: added %d, removed %d client aliases from system hosts file", added, removed)
}
// type check
var _ AddressUpdater = (*Storage)(nil)
// UpdateAddress implements the [AddressUpdater] interface for *Storage
func (s *Storage) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
// Common fast path optimization.
if host == "" && info == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if host != "" {
s.runtimeIndex.setInfo(ip, SourceRDNS, []string{host})
}
if info != nil {
s.setWHOISInfo(ip, info)
}
}
// UpdateDHCP updates [SourceDHCP] runtime client information.
func (s *Storage) UpdateDHCP() {
if s.dhcp == nil || !s.runtimeSourceDHCP {
return
}
s.mu.Lock()
defer s.mu.Unlock()
src := SourceDHCP
s.runtimeIndex.clearSource(src)
added := 0
for _, l := range s.dhcp.Leases() {
s.runtimeIndex.setInfo(l.IP, src, []string{l.Hostname})
added++
}
removed := s.runtimeIndex.removeEmpty()
log.Debug("storage: added %d, removed %d client aliases from dhcp", added, removed)
}
// setWHOISInfo sets the WHOIS information for a runtime client.
func (s *Storage) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
_, ok := s.index.findByIP(ip)
if ok {
log.Debug("storage: client for %s is already created, ignore whois info", ip)
return
}
rc := s.runtimeIndex.client(ip)
if rc == nil {
rc = NewRuntime(ip)
s.runtimeIndex.add(rc)
}
rc.setWHOIS(wi)
log.Debug("storage: set whois info for runtime client with ip %s: %+v", ip, wi)
}
// Add stores persistent client information or returns an error.
@@ -95,6 +408,9 @@ func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
// Find finds persistent client by string representation of the client ID, IP
// address, or MAC. And returns its shallow copy.
//
// TODO(s.chzhen): Accept ClientIDData structure instead, which will contain
// the parsed IP address, if any.
func (s *Storage) Find(id string) (p *Persistent, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -104,6 +420,16 @@ func (s *Storage) Find(id string) (p *Persistent, ok bool) {
return p.ShallowClone(), ok
}
ip, err := netip.ParseAddr(id)
if err != nil {
return nil, false
}
foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil {
return s.FindByMAC(foundMAC)
}
return nil, false
}
@@ -131,11 +457,9 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
return nil, false
}
// FindByMAC finds persistent client by MAC and returns its shallow copy.
// FindByMAC finds persistent client by MAC and returns its shallow copy. s.mu
// is expected to be locked.
func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok = s.index.findByMAC(mac)
if ok {
return p.ShallowClone(), ok
@@ -217,8 +541,8 @@ func (s *Storage) Size() (n int) {
return s.index.size()
}
// CloseUpstreams closes upstream configurations of persistent clients.
func (s *Storage) CloseUpstreams() (err error) {
// closeUpstreams closes upstream configurations of persistent clients.
func (s *Storage) closeUpstreams() (err error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -227,63 +551,39 @@ func (s *Storage) CloseUpstreams() (err error) {
// ClientRuntime returns a copy of the saved runtime client by ip. If no such
// client exists, returns nil.
//
// TODO(s.chzhen): Use it.
func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {
s.mu.Lock()
defer s.mu.Unlock()
return s.runtimeIndex.Client(ip)
}
rc = s.runtimeIndex.client(ip)
if rc != nil {
return rc.clone()
}
// AddRuntime saves the runtime client information in the storage. IP address
// of a client must be unique. rc must not be nil.
//
// TODO(s.chzhen): Use it.
func (s *Storage) AddRuntime(rc *Runtime) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.runtimeSourceDHCP {
return nil
}
s.runtimeIndex.Add(rc)
}
host := s.dhcp.HostByIP(ip)
if host == "" {
return nil
}
// SizeRuntime returns the number of the runtime clients.
//
// TODO(s.chzhen): Use it.
func (s *Storage) SizeRuntime() (n int) {
s.mu.Lock()
defer s.mu.Unlock()
rc = s.runtimeIndex.setInfo(ip, SourceDHCP, []string{host})
return s.runtimeIndex.Size()
return rc.clone()
}
// RangeRuntime calls f for each runtime client in an undefined order.
//
// TODO(s.chzhen): Use it.
func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
s.mu.Lock()
defer s.mu.Unlock()
s.runtimeIndex.Range(f)
s.runtimeIndex.rangeClients(f)
}
// DeleteRuntime removes the runtime client by ip.
//
// TODO(s.chzhen): Use it.
func (s *Storage) DeleteRuntime(ip netip.Addr) {
s.mu.Lock()
defer s.mu.Unlock()
s.runtimeIndex.Delete(ip)
}
// DeleteBySource removes all runtime clients that have information only from
// the specified source and returns the number of removed clients.
//
// TODO(s.chzhen): Use it.
func (s *Storage) DeleteBySource(src Source) (n int) {
s.mu.Lock()
defer s.mu.Unlock()
return s.runtimeIndex.DeleteBySource(src)
// AllowedTags returns the list of available client tags. tags must not be
// modified.
func (s *Storage) AllowedTags() (tags []string) {
return s.allowedTags
}

View File

@@ -3,28 +3,521 @@ package client_test
import (
"net"
"net/netip"
"runtime"
"slices"
"sync"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testHostsContainer is a mock implementation of the [client.HostsContainer]
// interface.
type testHostsContainer struct {
onUpd func() (updates <-chan *hostsfile.DefaultStorage)
}
// type check
var _ client.HostsContainer = (*testHostsContainer)(nil)
// Upd implements the [client.HostsContainer] interface for *testHostsContainer.
func (c *testHostsContainer) Upd() (updates <-chan *hostsfile.DefaultStorage) {
return c.onUpd()
}
// Interface stores and refreshes the network neighborhood reported by ARP
// (Address Resolution Protocol).
type Interface interface {
// Refresh updates the stored data. It must be safe for concurrent use.
Refresh() (err error)
// Neighbors returnes the last set of data reported by ARP. Both the method
// and it's result must be safe for concurrent use.
Neighbors() (ns []arpdb.Neighbor)
}
// testARPDB is a mock implementation of the [arpdb.Interface].
type testARPDB struct {
onRefresh func() (err error)
onNeighbors func() (ns []arpdb.Neighbor)
}
// type check
var _ arpdb.Interface = (*testARPDB)(nil)
// Refresh implements the [arpdb.Interface] interface for *testARP.
func (c *testARPDB) Refresh() (err error) {
return c.onRefresh()
}
// Neighbors implements the [arpdb.Interface] interface for *testARP.
func (c *testARPDB) Neighbors() (ns []arpdb.Neighbor) {
return c.onNeighbors()
}
// testDHCP is a mock implementation of the [client.DHCP].
type testDHCP struct {
OnLeases func() (leases []*dhcpsvc.Lease)
OnHostBy func(ip netip.Addr) (host string)
OnMACBy func(ip netip.Addr) (mac net.HardwareAddr)
}
// type check
var _ client.DHCP = (*testDHCP)(nil)
// Lease implements the [client.DHCP] interface for *testDHCP.
func (t *testDHCP) Leases() (leases []*dhcpsvc.Lease) { return t.OnLeases() }
// HostByIP implements the [client.DHCP] interface for *testDHCP.
func (t *testDHCP) HostByIP(ip netip.Addr) (host string) { return t.OnHostBy(ip) }
// MACByIP implements the [client.DHCP] interface for *testDHCP.
func (t *testDHCP) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { return t.OnMACBy(ip) }
// compareRuntimeInfo is a helper function that returns true if the runtime
// client has provided info.
func compareRuntimeInfo(rc *client.Runtime, src client.Source, host string) (ok bool) {
s, h := rc.Info()
if s != src {
return false
} else if h != host {
return false
}
return true
}
func TestStorage_Add_hostsfile(t *testing.T) {
var (
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliName1 = "client_one"
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliName2 = "client_two"
)
hostCh := make(chan *hostsfile.DefaultStorage)
h := &testHostsContainer{
onUpd: func() (updates <-chan *hostsfile.DefaultStorage) { return hostCh },
}
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
EtcHosts: h,
ARPClientsUpdatePeriod: testTimeout / 10,
})
require.NoError(t, err)
err = storage.Start(testutil.ContextWithTimeout(t, testTimeout))
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return storage.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
t.Run("add_hosts", func(t *testing.T) {
var s *hostsfile.DefaultStorage
s, err = hostsfile.NewDefaultStorage()
require.NoError(t, err)
s.Add(&hostsfile.Record{
Addr: cliIP1,
Names: []string{cliName1},
})
testutil.RequireSend(t, hostCh, s, testTimeout)
require.Eventually(t, func() (ok bool) {
cli1 := storage.ClientRuntime(cliIP1)
if cli1 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli1, client.SourceHostsFile, cliName1))
return true
}, testTimeout, testTimeout/10)
})
t.Run("update_hosts", func(t *testing.T) {
var s *hostsfile.DefaultStorage
s, err = hostsfile.NewDefaultStorage()
require.NoError(t, err)
s.Add(&hostsfile.Record{
Addr: cliIP2,
Names: []string{cliName2},
})
testutil.RequireSend(t, hostCh, s, testTimeout)
require.Eventually(t, func() (ok bool) {
cli2 := storage.ClientRuntime(cliIP2)
if cli2 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli2, client.SourceHostsFile, cliName2))
cli1 := storage.ClientRuntime(cliIP1)
require.Nil(t, cli1)
return true
}, testTimeout, testTimeout/10)
})
}
func TestStorage_Add_arp(t *testing.T) {
var (
mu sync.Mutex
neighbors []arpdb.Neighbor
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliName1 = "client_one"
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliName2 = "client_two"
)
a := &testARPDB{
onRefresh: func() (err error) { return nil },
onNeighbors: func() (ns []arpdb.Neighbor) {
mu.Lock()
defer mu.Unlock()
return neighbors
},
}
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
ARPDB: a,
ARPClientsUpdatePeriod: testTimeout / 10,
})
require.NoError(t, err)
err = storage.Start(testutil.ContextWithTimeout(t, testTimeout))
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return storage.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
t.Run("add_hosts", func(t *testing.T) {
func() {
mu.Lock()
defer mu.Unlock()
neighbors = []arpdb.Neighbor{{
Name: cliName1,
IP: cliIP1,
}}
}()
require.Eventually(t, func() (ok bool) {
cli1 := storage.ClientRuntime(cliIP1)
if cli1 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli1, client.SourceARP, cliName1))
return true
}, testTimeout, testTimeout/10)
})
t.Run("update_hosts", func(t *testing.T) {
func() {
mu.Lock()
defer mu.Unlock()
neighbors = []arpdb.Neighbor{{
Name: cliName2,
IP: cliIP2,
}}
}()
require.Eventually(t, func() (ok bool) {
cli2 := storage.ClientRuntime(cliIP2)
if cli2 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli2, client.SourceARP, cliName2))
cli1 := storage.ClientRuntime(cliIP1)
require.Nil(t, cli1)
return true
}, testTimeout, testTimeout/10)
})
}
func TestStorage_Add_whois(t *testing.T) {
var (
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliName2 = "client_two"
cliIP3 = netip.MustParseAddr("3.3.3.3")
cliName3 = "client_three"
)
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
})
require.NoError(t, err)
whois := &whois.Info{
Country: "AU",
Orgname: "Example Org",
}
t.Run("new_client", func(t *testing.T) {
storage.UpdateAddress(cliIP1, "", whois)
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
assert.Equal(t, whois, cli1.WHOIS())
})
t.Run("existing_runtime_client", func(t *testing.T) {
storage.UpdateAddress(cliIP2, cliName2, nil)
storage.UpdateAddress(cliIP2, "", whois)
cli2 := storage.ClientRuntime(cliIP2)
require.NotNil(t, cli2)
assert.True(t, compareRuntimeInfo(cli2, client.SourceRDNS, cliName2))
assert.Equal(t, whois, cli2.WHOIS())
})
t.Run("can't_set_persistent_client", func(t *testing.T) {
err = storage.Add(&client.Persistent{
Name: cliName3,
UID: client.MustNewUID(),
IPs: []netip.Addr{cliIP3},
})
require.NoError(t, err)
storage.UpdateAddress(cliIP3, "", whois)
rc := storage.ClientRuntime(cliIP3)
require.Nil(t, rc)
})
}
func TestClientsDHCP(t *testing.T) {
var (
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliName1 = "one.dhcp"
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliMAC2 = mustParseMAC("22:22:22:22:22:22")
cliName2 = "two.dhcp"
cliIP3 = netip.MustParseAddr("3.3.3.3")
cliMAC3 = mustParseMAC("33:33:33:33:33:33")
cliName3 = "three.dhcp"
prsCliIP = netip.MustParseAddr("4.3.2.1")
prsCliMAC = mustParseMAC("AA:AA:AA:AA:AA:AA")
prsCliName = "persistent.dhcp"
)
ipToHost := map[netip.Addr]string{
cliIP1: cliName1,
}
ipToMAC := map[netip.Addr]net.HardwareAddr{
prsCliIP: prsCliMAC,
}
leases := []*dhcpsvc.Lease{{
IP: cliIP2,
Hostname: cliName2,
HWAddr: cliMAC2,
}, {
IP: cliIP3,
Hostname: cliName3,
HWAddr: cliMAC3,
}}
d := &testDHCP{
OnLeases: func() (ls []*dhcpsvc.Lease) {
return leases
},
OnHostBy: func(ip netip.Addr) (host string) {
return ipToHost[ip]
},
OnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) {
return ipToMAC[ip]
},
}
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: d,
RuntimeSourceDHCP: true,
})
require.NoError(t, err)
t.Run("find_runtime", func(t *testing.T) {
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
assert.True(t, compareRuntimeInfo(cli1, client.SourceDHCP, cliName1))
})
t.Run("find_persistent", func(t *testing.T) {
err = storage.Add(&client.Persistent{
Name: prsCliName,
UID: client.MustNewUID(),
MACs: []net.HardwareAddr{prsCliMAC},
})
require.NoError(t, err)
prsCli, ok := storage.Find(prsCliIP.String())
require.True(t, ok)
assert.Equal(t, prsCliName, prsCli.Name)
})
t.Run("leases", func(t *testing.T) {
delete(ipToHost, cliIP1)
storage.UpdateDHCP()
cli1 := storage.ClientRuntime(cliIP1)
require.Nil(t, cli1)
for i, l := range leases {
cli := storage.ClientRuntime(l.IP)
require.NotNil(t, cli)
src, host := cli.Info()
assert.Equal(t, client.SourceDHCP, src)
assert.Equal(t, leases[i].Hostname, host)
}
})
t.Run("range", func(t *testing.T) {
s := 0
storage.RangeRuntime(func(rc *client.Runtime) (cont bool) {
s++
return true
})
assert.Equal(t, len(leases), s)
})
}
func TestClientsAddExisting(t *testing.T) {
t.Run("simple", func(t *testing.T) {
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
})
require.NoError(t, err)
ip := netip.MustParseAddr("1.1.1.1")
// Add a client.
err = storage.Add(&client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{ip, netip.MustParseAddr("1:2:3::4")},
Subnets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
MACs: []net.HardwareAddr{{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}},
})
require.NoError(t, err)
// Now add an auto-client with the same IP.
storage.UpdateAddress(ip, "test", nil)
rc := storage.ClientRuntime(ip)
assert.True(t, compareRuntimeInfo(rc, client.SourceRDNS, "test"))
})
t.Run("complicated", func(t *testing.T) {
// TODO(a.garipov): Properly decouple the DHCP server from the client
// storage.
if runtime.GOOS == "windows" {
t.Skip("skipping dhcp test on windows")
}
// First, init a DHCP server with a single static lease.
config := &dhcpd.ServerConfig{
Enabled: true,
DataDir: t.TempDir(),
Conf4: dhcpd.V4ServerConf{
Enabled: true,
GatewayIP: netip.MustParseAddr("1.2.3.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("1.2.3.2"),
RangeEnd: netip.MustParseAddr("1.2.3.10"),
},
}
dhcpServer, err := dhcpd.Create(config)
require.NoError(t, err)
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: dhcpServer,
})
require.NoError(t, err)
ip := netip.MustParseAddr("1.2.3.4")
err = dhcpServer.AddStaticLease(&dhcpsvc.Lease{
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: ip,
Hostname: "testhost",
Expiry: time.Now().Add(time.Hour),
})
require.NoError(t, err)
// Add a new client with the same IP as for a client with MAC.
err = storage.Add(&client.Persistent{
Name: "client2",
UID: client.MustNewUID(),
IPs: []netip.Addr{ip},
})
require.NoError(t, err)
// Add a new client with the IP from the first client's IP range.
err = storage.Add(&client.Persistent{
Name: "client3",
UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("2.2.2.2")},
})
require.NoError(t, err)
})
}
// newStorage is a helper function that returns a client storage filled with
// persistent clients from the m. It also generates a UID for each client.
func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
tb.Helper()
s = client.NewStorage(&client.Config{
AllowedTags: nil,
s, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
})
require.NoError(tb, err)
for _, c := range m {
c.UID = client.MustNewUID()
require.NoError(tb, s.Add(c))
}
require.Equal(tb, len(m), s.Size())
return s
}
@@ -43,6 +536,9 @@ func TestStorage_Add(t *testing.T) {
const (
existingName = "existing_name"
existingClientID = "existing_client_id"
allowedTag = "user_admin"
notAllowedTag = "not_allowed_tag"
)
var (
@@ -59,10 +555,20 @@ func TestStorage_Add(t *testing.T) {
UID: existingClientUID,
}
s := client.NewStorage(&client.Config{
AllowedTags: nil,
})
err := s.Add(existingClient)
s, err := client.NewStorage(&client.StorageConfig{})
require.NoError(t, err)
tags := s.AllowedTags()
require.NotZero(t, len(tags))
require.True(t, slices.IsSorted(tags))
_, ok := slices.BinarySearch(tags, allowedTag)
require.True(t, ok)
_, ok = slices.BinarySearch(tags, notAllowedTag)
require.False(t, ok)
err = s.Add(existingClient)
require.NoError(t, err)
testCases := []struct {
@@ -119,6 +625,46 @@ func TestStorage_Add(t *testing.T) {
},
wantErrMsg: `adding client: another client "existing_name" ` +
`uses the same ClientID "existing_client_id"`,
}, {
name: "not_allowed_tag",
cli: &client.Persistent{
Name: "not_allowed_tag",
Tags: []string{notAllowedTag},
IPs: []netip.Addr{netip.MustParseAddr("4.4.4.4")},
UID: client.MustNewUID(),
},
wantErrMsg: `adding client: invalid tag: "not_allowed_tag"`,
}, {
name: "allowed_tag",
cli: &client.Persistent{
Name: "allowed_tag",
Tags: []string{allowedTag},
IPs: []netip.Addr{netip.MustParseAddr("5.5.5.5")},
UID: client.MustNewUID(),
},
wantErrMsg: "",
}, {
name: "",
cli: &client.Persistent{
Name: "",
IPs: []netip.Addr{netip.MustParseAddr("6.6.6.6")},
UID: client.MustNewUID(),
},
wantErrMsg: "adding client: empty name",
}, {
name: "no_id",
cli: &client.Persistent{
Name: "no_id",
UID: client.MustNewUID(),
},
wantErrMsg: "adding client: id required",
}, {
name: "no_uid",
cli: &client.Persistent{
Name: "no_uid",
IPs: []netip.Addr{netip.MustParseAddr("7.7.7.7")},
},
wantErrMsg: "adding client: uid required",
}}
for _, tc := range testCases {
@@ -141,10 +687,10 @@ func TestStorage_RemoveByName(t *testing.T) {
UID: client.MustNewUID(),
}
s := client.NewStorage(&client.Config{
AllowedTags: nil,
})
err := s.Add(existingClient)
s, err := client.NewStorage(&client.StorageConfig{})
require.NoError(t, err)
err = s.Add(existingClient)
require.NoError(t, err)
testCases := []struct {
@@ -168,9 +714,9 @@ func TestStorage_RemoveByName(t *testing.T) {
}
t.Run("duplicate_remove", func(t *testing.T) {
s = client.NewStorage(&client.Config{
AllowedTags: nil,
})
s, err = client.NewStorage(&client.StorageConfig{})
require.NoError(t, err)
err = s.Add(existingClient)
require.NoError(t, err)
@@ -341,6 +887,127 @@ func TestStorage_FindLoose(t *testing.T) {
}
}
func TestStorage_FindByName(t *testing.T) {
const (
cliIP1 = "1.1.1.1"
cliIP2 = "2.2.2.2"
)
const (
clientExistingName = "client_existing"
clientAnotherExistingName = "client_another_existing"
nonExistingClientName = "client_non_existing"
)
var (
clientExisting = &client.Persistent{
Name: clientExistingName,
IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
}
clientAnotherExisting = &client.Persistent{
Name: clientAnotherExistingName,
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
}
)
clients := []*client.Persistent{
clientExisting,
clientAnotherExisting,
}
s := newStorage(t, clients)
testCases := []struct {
want *client.Persistent
name string
clientName string
}{{
name: "existing",
clientName: clientExistingName,
want: clientExisting,
}, {
name: "another_existing",
clientName: clientAnotherExistingName,
want: clientAnotherExisting,
}, {
name: "non_existing",
clientName: nonExistingClientName,
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := s.FindByName(tc.clientName)
if tc.want == nil {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, tc.want, c)
})
}
}
func TestStorage_FindByMAC(t *testing.T) {
var (
cliMAC = mustParseMAC("11:11:11:11:11:11")
cliAnotherMAC = mustParseMAC("22:22:22:22:22:22")
nonExistingClientMAC = mustParseMAC("33:33:33:33:33:33")
)
var (
clientExisting = &client.Persistent{
Name: "client",
MACs: []net.HardwareAddr{cliMAC},
}
clientAnotherExisting = &client.Persistent{
Name: "another_client",
MACs: []net.HardwareAddr{cliAnotherMAC},
}
)
clients := []*client.Persistent{
clientExisting,
clientAnotherExisting,
}
s := newStorage(t, clients)
testCases := []struct {
want *client.Persistent
name string
clientMAC net.HardwareAddr
}{{
name: "existing",
clientMAC: cliMAC,
want: clientExisting,
}, {
name: "another_existing",
clientMAC: cliAnotherMAC,
want: clientAnotherExisting,
}, {
name: "non_existing",
clientMAC: nonExistingClientMAC,
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := s.FindByMAC(tc.clientMAC)
if tc.want == nil {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, tc.want, c)
})
}
}
func TestStorage_Update(t *testing.T) {
const (
clientName = "client_name"

View File

@@ -3,6 +3,7 @@ package dhcpsvc
import (
"fmt"
"log/slog"
"os"
"time"
"github.com/AdguardTeam/golibs/errors"
@@ -23,7 +24,8 @@ type Config struct {
// clients' hostnames.
LocalDomainName string
// TODO(e.burkov): Add DB path.
// DBFilePath is the path to the database file containing the DHCP leases.
DBFilePath string
// ICMPTimeout is the timeout for checking another DHCP server's presence.
ICMPTimeout time.Duration
@@ -64,6 +66,12 @@ func (conf *Config) Validate() (err error) {
errs = append(errs, err)
}
// This is a best-effort check for the file accessibility. The file will be
// checked again when it is opened later.
if _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) {
errs = append(errs, fmt.Errorf("db file path %q: %w", conf.DBFilePath, err))
}
if len(conf.Interfaces) == 0 {
errs = append(errs, errNoInterfaces)

View File

@@ -1,6 +1,7 @@
package dhcpsvc_test
import (
"path/filepath"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
@@ -8,6 +9,8 @@ import (
)
func TestConfig_Validate(t *testing.T) {
leasesPath := filepath.Join(t.TempDir(), "leases.json")
testCases := []struct {
name string
conf *dhcpsvc.Config
@@ -25,6 +28,7 @@ func TestConfig_Validate(t *testing.T) {
conf: &dhcpsvc.Config{
Enabled: true,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
},
wantErrMsg: `bad domain name "": domain name is empty`,
}, {
@@ -32,6 +36,7 @@ func TestConfig_Validate(t *testing.T) {
Enabled: true,
LocalDomainName: testLocalTLD,
Interfaces: nil,
DBFilePath: leasesPath,
},
name: "no_interfaces",
wantErrMsg: "no interfaces specified",
@@ -40,6 +45,7 @@ func TestConfig_Validate(t *testing.T) {
Enabled: true,
LocalDomainName: testLocalTLD,
Interfaces: nil,
DBFilePath: leasesPath,
},
name: "no_interfaces",
wantErrMsg: "no interfaces specified",
@@ -50,6 +56,7 @@ func TestConfig_Validate(t *testing.T) {
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
"eth0": nil,
},
DBFilePath: leasesPath,
},
name: "nil_interface",
wantErrMsg: `interface "eth0": config is nil`,
@@ -63,6 +70,7 @@ func TestConfig_Validate(t *testing.T) {
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
},
},
DBFilePath: leasesPath,
},
name: "nil_ipv4",
wantErrMsg: `interface "eth0": ipv4: config is nil`,
@@ -76,6 +84,7 @@ func TestConfig_Validate(t *testing.T) {
IPv6: nil,
},
},
DBFilePath: leasesPath,
},
name: "nil_ipv6",
wantErrMsg: `interface "eth0": ipv6: config is nil`,

195
internal/dhcpsvc/db.go Normal file
View File

@@ -0,0 +1,195 @@
package dhcpsvc
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/netip"
"os"
"slices"
"strings"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/google/renameio/v2/maybe"
)
// dataVersion is the current version of the stored DHCP leases structure.
const dataVersion = 1
// databasePerm is the permissions for the database file.
const databasePerm fs.FileMode = 0o640
// dataLeases is the structure of the stored DHCP leases.
type dataLeases struct {
// Leases is the list containing stored DHCP leases.
Leases []*dbLease `json:"leases"`
// Version is the current version of the structure.
Version int `json:"version"`
}
// dbLease is the structure of stored lease.
type dbLease struct {
Expiry string `json:"expires"`
IP netip.Addr `json:"ip"`
Hostname string `json:"hostname"`
HWAddr string `json:"mac"`
IsStatic bool `json:"static"`
}
// compareNames returns the result of comparing the hostnames of dl and other
// lexicographically.
func (dl *dbLease) compareNames(other *dbLease) (res int) {
return strings.Compare(dl.Hostname, other.Hostname)
}
// toDBLease converts *Lease to *dbLease.
func toDBLease(l *Lease) (dl *dbLease) {
var expiryStr string
if !l.IsStatic {
// The front-end is waiting for RFC 3999 format of the time value. It
// also shouldn't got an Expiry field for static leases.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.
expiryStr = l.Expiry.Format(time.RFC3339)
}
return &dbLease{
Expiry: expiryStr,
Hostname: l.Hostname,
HWAddr: l.HWAddr.String(),
IP: l.IP,
IsStatic: l.IsStatic,
}
}
// toInternal converts dl to *Lease.
func (dl *dbLease) toInternal() (l *Lease, err error) {
mac, err := net.ParseMAC(dl.HWAddr)
if err != nil {
return nil, fmt.Errorf("parsing hardware address: %w", err)
}
expiry := time.Time{}
if !dl.IsStatic {
expiry, err = time.Parse(time.RFC3339, dl.Expiry)
if err != nil {
return nil, fmt.Errorf("parsing expiry time: %w", err)
}
}
return &Lease{
Expiry: expiry,
IP: dl.IP,
Hostname: dl.Hostname,
HWAddr: mac,
IsStatic: dl.IsStatic,
}, nil
}
// dbLoad loads stored leases. It must only be called before the service has
// been started.
func (srv *DHCPServer) dbLoad(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "loading db: %w") }()
file, err := os.Open(srv.dbFilePath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("reading db: %w", err)
}
srv.logger.DebugContext(ctx, "no db file found")
return nil
}
defer func() {
err = errors.WithDeferred(err, file.Close())
}()
dl := &dataLeases{}
err = json.NewDecoder(file).Decode(dl)
if err != nil {
return fmt.Errorf("decoding db: %w", err)
}
srv.resetLeases()
srv.addDBLeases(ctx, dl.Leases)
return nil
}
// addDBLeases adds leases to the server.
func (srv *DHCPServer) addDBLeases(ctx context.Context, leases []*dbLease) {
var v4, v6 uint
for i, l := range leases {
lease, err := l.toInternal()
if err != nil {
srv.logger.WarnContext(ctx, "converting lease", "idx", i, slogutil.KeyError, err)
continue
}
iface, err := srv.ifaceForAddr(l.IP)
if err != nil {
srv.logger.WarnContext(ctx, "searching lease iface", "idx", i, slogutil.KeyError, err)
continue
}
err = srv.leases.add(lease, iface)
if err != nil {
srv.logger.WarnContext(ctx, "adding lease", "idx", i, slogutil.KeyError, err)
continue
}
if l.IP.Is4() {
v4++
} else {
v6++
}
}
// TODO(e.burkov): Group by interface.
srv.logger.InfoContext(ctx, "loaded leases", "v4", v4, "v6", v6, "total", len(leases))
}
// writeDB writes leases to the database file. It expects the
// [DHCPServer.leasesMu] to be locked.
func (srv *DHCPServer) dbStore(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "writing db: %w") }()
dl := &dataLeases{
// Avoid writing null into the database file if there are no leases.
Leases: make([]*dbLease, 0, srv.leases.len()),
Version: dataVersion,
}
srv.leases.rangeLeases(func(l *Lease) (cont bool) {
lease := toDBLease(l)
i, _ := slices.BinarySearchFunc(dl.Leases, lease, (*dbLease).compareNames)
dl.Leases = slices.Insert(dl.Leases, i, lease)
return true
})
buf, err := json.Marshal(dl)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
err = maybe.WriteFile(srv.dbFilePath, buf, databasePerm)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
srv.logger.InfoContext(ctx, "stored leases", "num", len(dl.Leases), "file", srv.dbFilePath)
return nil
}

View File

@@ -0,0 +1,4 @@
package dhcpsvc
// DatabasePerm is the permissions for the test database file.
const DatabasePerm = databasePerm

View File

@@ -50,7 +50,7 @@ type Interface interface {
IPByHost(host string) (ip netip.Addr)
// Leases returns all the active DHCP leases. The returned slice should be
// a clone.
// a clone. The order of leases is undefined.
//
// TODO(e.burkov): Consider implementing iterating methods with appropriate
// signatures instead of cloning the whole list.

View File

@@ -0,0 +1,66 @@
package dhcpsvc_test
import (
"net"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/require"
)
// testLocalTLD is a common local TLD for tests.
const testLocalTLD = "local"
// testTimeout is a common timeout for tests and contexts.
const testTimeout time.Duration = 10 * time.Second
// discardLog is a logger to discard test output.
var discardLog = slogutil.NewDiscardLogger()
// testInterfaceConf is a common set of interface configurations for tests.
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
"eth0": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("192.168.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("192.168.0.2"),
RangeEnd: netip.MustParseAddr("192.168.0.254"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db8::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
"eth1": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("172.16.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("172.16.0.2"),
RangeEnd: netip.MustParseAddr("172.16.0.255"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db9::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
}
// mustParseMAC parses a hardware address from s and requires no errors.
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
require.NoError(t, err)
return mac
}

View File

@@ -3,42 +3,74 @@ package dhcpsvc
import (
"fmt"
"log/slog"
"slices"
"net"
"time"
)
// netInterface is a common part of any network interface within the DHCP
// server.
// macKey contains hardware address as byte array of 6, 8, or 20 bytes.
//
// TODO(e.burkov): Move to aghnet or even to netutil.
type macKey any
// macToKey converts mac into macKey, which is used as the key for the lease
// maps. mac must be a valid hardware address of length 6, 8, or 20 bytes, see
// [netutil.ValidateMAC].
func macToKey(mac net.HardwareAddr) (key macKey) {
switch len(mac) {
case 6:
return [6]byte(mac)
case 8:
return [8]byte(mac)
case 20:
return [20]byte(mac)
default:
panic(fmt.Errorf("invalid mac address %#v", mac))
}
}
// netInterface is a common part of any interface within the DHCP server.
//
// TODO(e.burkov): Add other methods as [DHCPServer] evolves.
type netInterface struct {
// logger logs the events related to the network interface.
logger *slog.Logger
// leases is the set of DHCP leases assigned to this interface.
leases map[macKey]*Lease
// name is the name of the network interface.
name string
// leases is a set of leases sorted by hardware address.
leases []*Lease
// leaseTTL is the default Time-To-Live value for leases.
leaseTTL time.Duration
}
// reset clears all the slices in iface for reuse.
func (iface *netInterface) reset() {
iface.leases = iface.leases[:0]
// newNetInterface creates a new netInterface with the given name, leaseTTL, and
// logger.
func newNetInterface(name string, l *slog.Logger, leaseTTL time.Duration) (iface *netInterface) {
return &netInterface{
logger: l,
leases: map[macKey]*Lease{},
name: name,
leaseTTL: leaseTTL,
}
}
// insertLease inserts the given lease into iface. It returns an error if the
// reset clears all the slices in iface for reuse.
func (iface *netInterface) reset() {
clear(iface.leases)
}
// addLease inserts the given lease into iface. It returns an error if the
// lease can't be inserted.
func (iface *netInterface) insertLease(l *Lease) (err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
func (iface *netInterface) addLease(l *Lease) (err error) {
mk := macToKey(l.HWAddr)
_, found := iface.leases[mk]
if found {
return fmt.Errorf("lease for mac %s already exists", l.HWAddr)
}
iface.leases = slices.Insert(iface.leases, i, l)
iface.leases[mk] = l
return nil
}
@@ -46,12 +78,13 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
// updateLease replaces an existing lease within iface with the given one. It
// returns an error if there is no lease with such hardware address.
func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
mk := macToKey(l.HWAddr)
prev, found := iface.leases[mk]
if !found {
return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
}
prev, iface.leases[i] = iface.leases[i], l
iface.leases[mk] = l
return prev, nil
}
@@ -59,12 +92,13 @@ func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
// removeLease removes an existing lease from iface. It returns an error if
// there is no lease equal to l.
func (iface *netInterface) removeLease(l *Lease) (err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
mk := macToKey(l.HWAddr)
_, found := iface.leases[mk]
if !found {
return fmt.Errorf("no lease for mac %s", l.HWAddr)
}
iface.leases = slices.Delete(iface.leases, i, i+1)
delete(iface.leases, mk)
return nil
}

View File

@@ -1,7 +1,6 @@
package dhcpsvc
import (
"bytes"
"net"
"net/netip"
"slices"
@@ -45,8 +44,3 @@ func (l *Lease) Clone() (clone *Lease) {
IsStatic: l.IsStatic,
}
}
// compareLeaseMAC compares two [Lease]s by hardware address.
func compareLeaseMAC(a, b *Lease) (res int) {
return bytes.Compare(a.HWAddr, b.HWAddr)
}

View File

@@ -61,7 +61,7 @@ func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
}
err = iface.insertLease(l)
err = iface.addLease(l)
if err != nil {
return err
}
@@ -124,3 +124,18 @@ func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) {
return nil
}
// rangeLeases calls f for each lease in idx in an unspecified order until f
// returns false.
func (idx *leaseIndex) rangeLeases(f func(l *Lease) (cont bool)) {
for _, l := range idx.byName {
if !f(l) {
break
}
}
}
// len returns the number of leases in idx.
func (idx *leaseIndex) len() (l uint) {
return uint(len(idx.byAddr))
}

View File

@@ -27,6 +27,13 @@ type DHCPServer struct {
// hostnames.
localTLD string
// dbFilePath is the path to the database file containing the DHCP leases.
//
// TODO(e.burkov): Consider extracting the database logic into a separate
// interface to prevent packages that only need lease data from depending on
// the entire server and to simplify testing.
dbFilePath string
// leasesMu protects the leases index as well as leases in the interfaces.
leasesMu *sync.RWMutex
@@ -34,10 +41,10 @@ type DHCPServer struct {
leases *leaseIndex
// interfaces4 is the set of IPv4 interfaces sorted by interface name.
interfaces4 netInterfacesV4
interfaces4 dhcpInterfacesV4
// interfaces6 is the set of IPv6 interfaces sorted by interface name.
interfaces6 netInterfacesV6
interfaces6 dhcpInterfacesV6
// icmpTimeout is the timeout for checking another DHCP server's presence.
icmpTimeout time.Duration
@@ -56,28 +63,9 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
return nil, nil
}
// TODO(e.burkov): Add validations scoped to the network interfaces set.
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces))
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces))
var errs []error
mapsutil.SortedRange(conf.Interfaces, func(name string, iface *InterfaceConfig) (cont bool) {
var i4 *netInterfaceV4
i4, err = newNetInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
ifaces4 = append(ifaces4, i4)
}
i6 := newNetInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
ifaces6 = append(ifaces6, i6)
}
return true
})
if err = errors.Join(errs...); err != nil {
ifaces4, ifaces6, err := newInterfaces(ctx, l, conf.Interfaces)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
@@ -93,13 +81,55 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
interfaces4: ifaces4,
interfaces6: ifaces6,
icmpTimeout: conf.ICMPTimeout,
dbFilePath: conf.DBFilePath,
}
// TODO(e.burkov): Load leases.
err = srv.dbLoad(ctx)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
return srv, nil
}
// newInterfaces creates interfaces for the given map of interface names to
// their configurations.
func newInterfaces(
ctx context.Context,
l *slog.Logger,
ifaces map[string]*InterfaceConfig,
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
// TODO(e.burkov): Add validations scoped to the network interfaces set.
v4 = make(dhcpInterfacesV4, 0, len(ifaces))
v6 = make(dhcpInterfacesV6, 0, len(ifaces))
var errs []error
mapsutil.SortedRange(ifaces, func(name string, iface *InterfaceConfig) (cont bool) {
var i4 *dhcpInterfaceV4
i4, err = newDHCPInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
v4 = append(v4, i4)
}
i6 := newDHCPInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
v6 = append(v6, i6)
}
return true
})
if err = errors.Join(errs...); err != nil {
return nil, nil, err
}
return v4, v6, nil
}
// type check
//
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
@@ -115,16 +145,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock()
for _, iface := range srv.interfaces4 {
for _, lease := range iface.leases {
leases = append(leases, lease.Clone())
}
}
for _, iface := range srv.interfaces6 {
for _, lease := range iface.leases {
leases = append(leases, lease.Clone())
}
}
srv.leases.rangeLeases(func(l *Lease) (cont bool) {
leases = append(leases, l.Clone())
return true
})
return leases
}
@@ -167,22 +192,35 @@ func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
// Reset implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) Reset(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "resetting leases: %w") }()
srv.leasesMu.Lock()
defer srv.leasesMu.Unlock()
for _, iface := range srv.interfaces4 {
iface.reset()
srv.resetLeases()
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
return err
}
for _, iface := range srv.interfaces6 {
iface.reset()
}
srv.leases.clear()
srv.logger.DebugContext(ctx, "reset leases")
return nil
}
// resetLeases resets the leases for all network interfaces of the server. It
// expects the DHCPServer.leasesMu to be locked.
func (srv *DHCPServer) resetLeases() {
for _, iface := range srv.interfaces4 {
iface.common.reset()
}
for _, iface := range srv.interfaces6 {
iface.common.reset()
}
srv.leases.clear()
}
// AddLease implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "adding lease: %w") }()
@@ -190,7 +228,7 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
addr := l.IP
iface, err := srv.ifaceForAddr(addr)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
// Don't wrap the error since it's already informative enough as is.
return err
}
@@ -203,6 +241,12 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
return err
}
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext(
ctx, "added lease",
"hostname", l.Hostname,
@@ -223,7 +267,7 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
addr := l.IP
iface, err := srv.ifaceForAddr(addr)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
// Don't wrap the error since it's already informative enough as is.
return err
}
@@ -236,6 +280,12 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
return err
}
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext(
ctx, "updated lease",
"hostname", l.Hostname,
@@ -254,7 +304,7 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
addr := l.IP
iface, err := srv.ifaceForAddr(addr)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
// Don't wrap the error since it's already informative enough as is.
return err
}
@@ -267,6 +317,12 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
return err
}
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext(
ctx, "removed lease",
"hostname", l.Hostname,

View File

@@ -1,72 +1,41 @@
package dhcpsvc_test
import (
"net"
"io/fs"
"net/netip"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testLocalTLD is a common local TLD for tests.
const testLocalTLD = "local"
// testdata is a filesystem containing data for tests.
var testdata = os.DirFS("testdata")
// testTimeout is a common timeout for tests and contexts.
const testTimeout time.Duration = 10 * time.Second
// newTempDB copies the leases database file located in the testdata FS, under
// tb.Name()/leases.json, to a temporary directory and returns the path to the
// copied file.
func newTempDB(tb testing.TB) (dst string) {
tb.Helper()
// discardLog is a logger to discard test output.
var discardLog = slogutil.NewDiscardLogger()
const filename = "leases.json"
// testInterfaceConf is a common set of interface configurations for tests.
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
"eth0": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("192.168.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("192.168.0.2"),
RangeEnd: netip.MustParseAddr("192.168.0.254"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db8::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
"eth1": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("172.16.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("172.16.0.2"),
RangeEnd: netip.MustParseAddr("172.16.0.255"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db9::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
}
data, err := fs.ReadFile(testdata, path.Join(tb.Name(), filename))
require.NoError(tb, err)
// mustParseMAC parses a hardware address from s and requires no errors.
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
require.NoError(t, err)
dst = filepath.Join(tb.TempDir(), filename)
return mac
err = os.WriteFile(dst, data, dhcpsvc.DatabasePerm)
require.NoError(tb, err)
return dst
}
func TestNew(t *testing.T) {
@@ -103,6 +72,8 @@ func TestNew(t *testing.T) {
RASLAACOnly: true,
}
leasesPath := filepath.Join(t.TempDir(), "leases.json")
testCases := []struct {
conf *dhcpsvc.Config
name string
@@ -118,6 +89,7 @@ func TestNew(t *testing.T) {
IPv6: validIPv6Conf,
},
},
DBFilePath: leasesPath,
},
name: "valid",
wantErrMsg: "",
@@ -132,6 +104,7 @@ func TestNew(t *testing.T) {
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
},
},
DBFilePath: leasesPath,
},
name: "disabled_interfaces",
wantErrMsg: "",
@@ -146,9 +119,10 @@ func TestNew(t *testing.T) {
IPv6: validIPv6Conf,
},
},
DBFilePath: leasesPath,
},
name: "gateway_within_range",
wantErrMsg: `interface "eth0": ipv4: ` +
wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
`gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`,
}, {
conf: &dhcpsvc.Config{
@@ -161,9 +135,10 @@ func TestNew(t *testing.T) {
IPv6: validIPv6Conf,
},
},
DBFilePath: leasesPath,
},
name: "bad_start",
wantErrMsg: `interface "eth0": ipv4: ` +
wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
`range start 127.0.0.1 is not within 192.168.0.1/24`,
}}
@@ -180,32 +155,36 @@ func TestNew(t *testing.T) {
func TestDHCPServer_AddLease(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := filepath.Join(t.TempDir(), "leases.json")
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
})
require.NoError(t, err)
const (
host1 = "host1"
host2 = "host2"
host3 = "host3"
existHost = "host1"
newHost = "host2"
ipv6Host = "host3"
)
ip1 := netip.MustParseAddr("192.168.0.2")
ip2 := netip.MustParseAddr("192.168.0.3")
ip3 := netip.MustParseAddr("2001:db8::2")
var (
existIP = netip.MustParseAddr("192.168.0.2")
newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
newMAC = mustParseMAC(t, "06:05:04:03:02:01")
ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07")
)
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
Hostname: existHost,
IP: existIP,
HWAddr: existMAC,
IsStatic: true,
}))
@@ -216,61 +195,61 @@ func TestDHCPServer_AddLease(t *testing.T) {
}{{
name: "outside_range",
lease: &dhcpsvc.Lease{
Hostname: host2,
Hostname: newHost,
IP: netip.MustParseAddr("1.2.3.4"),
HWAddr: mac2,
HWAddr: newMAC,
},
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
}, {
name: "duplicate_ip",
lease: &dhcpsvc.Lease{
Hostname: host2,
IP: ip1,
HWAddr: mac2,
Hostname: newHost,
IP: existIP,
HWAddr: newMAC,
},
wantErrMsg: "adding lease: lease for ip " + ip1.String() +
wantErrMsg: "adding lease: lease for ip " + existIP.String() +
" already exists",
}, {
name: "duplicate_hostname",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: ip2,
HWAddr: mac2,
Hostname: existHost,
IP: newIP,
HWAddr: newMAC,
},
wantErrMsg: "adding lease: lease for hostname " + host1 +
wantErrMsg: "adding lease: lease for hostname " + existHost +
" already exists",
}, {
name: "duplicate_hostname_case",
lease: &dhcpsvc.Lease{
Hostname: strings.ToUpper(host1),
IP: ip2,
HWAddr: mac2,
Hostname: strings.ToUpper(existHost),
IP: newIP,
HWAddr: newMAC,
},
wantErrMsg: "adding lease: lease for hostname " +
strings.ToUpper(host1) + " already exists",
strings.ToUpper(existHost) + " already exists",
}, {
name: "duplicate_mac",
lease: &dhcpsvc.Lease{
Hostname: host2,
IP: ip2,
HWAddr: mac1,
Hostname: newHost,
IP: newIP,
HWAddr: existMAC,
},
wantErrMsg: "adding lease: lease for mac " + mac1.String() +
wantErrMsg: "adding lease: lease for mac " + existMAC.String() +
" already exists",
}, {
name: "valid",
lease: &dhcpsvc.Lease{
Hostname: host2,
IP: ip2,
HWAddr: mac2,
Hostname: newHost,
IP: newIP,
HWAddr: newMAC,
},
wantErrMsg: "",
}, {
name: "valid_v6",
lease: &dhcpsvc.Lease{
Hostname: host3,
IP: ip3,
HWAddr: mac3,
Hostname: ipv6Host,
IP: newIPv6,
HWAddr: ipv6MAC,
},
wantErrMsg: "",
}}
@@ -280,16 +259,21 @@ func TestDHCPServer_AddLease(t *testing.T) {
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease))
})
}
assert.NotEmpty(t, srv.Leases())
assert.FileExists(t, leasesPath)
}
func TestDHCPServer_index(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := newTempDB(t)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
})
require.NoError(t, err)
@@ -301,46 +285,23 @@ func TestDHCPServer_index(t *testing.T) {
host5 = "host5"
)
ip1 := netip.MustParseAddr("192.168.0.2")
ip2 := netip.MustParseAddr("192.168.0.3")
ip3 := netip.MustParseAddr("172.16.0.3")
ip4 := netip.MustParseAddr("172.16.0.4")
var (
ip1 = netip.MustParseAddr("192.168.0.2")
ip2 = netip.MustParseAddr("192.168.0.3")
ip3 = netip.MustParseAddr("172.16.0.3")
ip4 = netip.MustParseAddr("172.16.0.4")
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
leases := []*dhcpsvc.Lease{{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IsStatic: true,
}, {
Hostname: host2,
IP: ip2,
HWAddr: mac2,
IsStatic: true,
}, {
Hostname: host3,
IP: ip3,
HWAddr: mac3,
IsStatic: true,
}, {
Hostname: host4,
IP: ip4,
HWAddr: mac1,
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
}
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
mac3 = mustParseMAC(t, "02:03:04:05:06:07")
)
t.Run("ip_idx", func(t *testing.T) {
assert.Equal(t, ip1, srv.IPByHost(host1))
assert.Equal(t, ip2, srv.IPByHost(host2))
assert.Equal(t, ip3, srv.IPByHost(host3))
assert.Equal(t, ip4, srv.IPByHost(host4))
assert.Equal(t, netip.Addr{}, srv.IPByHost(host5))
assert.Zero(t, srv.IPByHost(host5))
})
t.Run("name_idx", func(t *testing.T) {
@@ -348,7 +309,7 @@ func TestDHCPServer_index(t *testing.T) {
assert.Equal(t, host2, srv.HostByIP(ip2))
assert.Equal(t, host3, srv.HostByIP(ip3))
assert.Equal(t, host4, srv.HostByIP(ip4))
assert.Equal(t, "", srv.HostByIP(netip.Addr{}))
assert.Zero(t, srv.HostByIP(netip.Addr{}))
})
t.Run("mac_idx", func(t *testing.T) {
@@ -356,18 +317,20 @@ func TestDHCPServer_index(t *testing.T) {
assert.Equal(t, mac2, srv.MACByIP(ip2))
assert.Equal(t, mac3, srv.MACByIP(ip3))
assert.Equal(t, mac1, srv.MACByIP(ip4))
assert.Nil(t, srv.MACByIP(netip.Addr{}))
assert.Zero(t, srv.MACByIP(netip.Addr{}))
})
}
func TestDHCPServer_UpdateStaticLease(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := newTempDB(t)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
})
require.NoError(t, err)
@@ -380,36 +343,16 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
host6 = "host6"
)
ip1 := netip.MustParseAddr("192.168.0.2")
ip2 := netip.MustParseAddr("192.168.0.3")
ip3 := netip.MustParseAddr("192.168.0.4")
ip4 := netip.MustParseAddr("2001:db8::2")
ip5 := netip.MustParseAddr("2001:db8::3")
var (
ip1 = netip.MustParseAddr("192.168.0.2")
ip2 = netip.MustParseAddr("192.168.0.3")
ip3 = netip.MustParseAddr("192.168.0.4")
ip4 = netip.MustParseAddr("2001:db8::3")
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "01:02:03:04:05:07")
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
mac4 := mustParseMAC(t, "06:05:04:03:02:02")
leases := []*dhcpsvc.Lease{{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IsStatic: true,
}, {
Hostname: host2,
IP: ip2,
HWAddr: mac2,
IsStatic: true,
}, {
Hostname: host4,
IP: ip4,
HWAddr: mac4,
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
}
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
mac3 = mustParseMAC(t, "06:05:04:03:02:02")
)
testCases := []struct {
name string
@@ -428,9 +371,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
lease: &dhcpsvc.Lease{
Hostname: host3,
IP: ip3,
HWAddr: mac3,
HWAddr: mac2,
},
wantErrMsg: "updating static lease: no lease for mac " + mac3.String(),
wantErrMsg: "updating static lease: no lease for mac " + mac2.String(),
}, {
name: "duplicate_ip",
lease: &dhcpsvc.Lease{
@@ -470,8 +413,8 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
name: "valid_v6",
lease: &dhcpsvc.Lease{
Hostname: host6,
IP: ip5,
HWAddr: mac4,
IP: ip4,
HWAddr: mac3,
},
wantErrMsg: "",
}}
@@ -481,16 +424,20 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))
})
}
assert.FileExists(t, leasesPath)
}
func TestDHCPServer_RemoveLease(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := newTempDB(t)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
})
require.NoError(t, err)
@@ -500,28 +447,15 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
host3 = "host3"
)
ip1 := netip.MustParseAddr("192.168.0.2")
ip2 := netip.MustParseAddr("192.168.0.3")
ip3 := netip.MustParseAddr("2001:db8::2")
var (
existIP = netip.MustParseAddr("192.168.0.2")
newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "02:03:04:05:06:07")
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
leases := []*dhcpsvc.Lease{{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IsStatic: true,
}, {
Hostname: host3,
IP: ip3,
HWAddr: mac3,
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
}
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
newMAC = mustParseMAC(t, "02:03:04:05:06:07")
ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01")
)
testCases := []struct {
name string
@@ -531,40 +465,40 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
name: "not_found_mac",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: ip1,
HWAddr: mac2,
IP: existIP,
HWAddr: newMAC,
},
wantErrMsg: "removing lease: no lease for mac " + mac2.String(),
wantErrMsg: "removing lease: no lease for mac " + newMAC.String(),
}, {
name: "not_found_ip",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: ip2,
HWAddr: mac1,
IP: newIP,
HWAddr: existMAC,
},
wantErrMsg: "removing lease: no lease for ip " + ip2.String(),
wantErrMsg: "removing lease: no lease for ip " + newIP.String(),
}, {
name: "not_found_host",
lease: &dhcpsvc.Lease{
Hostname: host2,
IP: ip1,
HWAddr: mac1,
IP: existIP,
HWAddr: existMAC,
},
wantErrMsg: "removing lease: no lease for hostname " + host2,
}, {
name: "valid",
lease: &dhcpsvc.Lease{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IP: existIP,
HWAddr: existMAC,
},
wantErrMsg: "",
}, {
name: "valid_v6",
lease: &dhcpsvc.Lease{
Hostname: host3,
IP: ip3,
HWAddr: mac3,
IP: newIPv6,
HWAddr: ipv6MAC,
},
wantErrMsg: "",
}}
@@ -575,49 +509,64 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
})
}
assert.FileExists(t, leasesPath)
assert.Empty(t, srv.Leases())
}
func TestDHCPServer_Reset(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
leasesPath := newTempDB(t)
conf := &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
})
require.NoError(t, err)
leases := []*dhcpsvc.Lease{{
Hostname: "host1",
IP: netip.MustParseAddr("192.168.0.2"),
HWAddr: mustParseMAC(t, "01:02:03:04:05:06"),
IsStatic: true,
}, {
Hostname: "host2",
IP: netip.MustParseAddr("192.168.0.3"),
HWAddr: mustParseMAC(t, "06:05:04:03:02:01"),
IsStatic: true,
}, {
Hostname: "host3",
IP: netip.MustParseAddr("2001:db8::2"),
HWAddr: mustParseMAC(t, "02:03:04:05:06:07"),
IsStatic: true,
}, {
Hostname: "host4",
IP: netip.MustParseAddr("2001:db8::3"),
HWAddr: mustParseMAC(t, "06:05:04:03:02:02"),
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
DBFilePath: leasesPath,
}
require.Len(t, srv.Leases(), len(leases))
ctx := testutil.ContextWithTimeout(t, testTimeout)
srv, err := dhcpsvc.New(ctx, conf)
require.NoError(t, err)
const leasesNum = 4
require.Len(t, srv.Leases(), leasesNum)
require.NoError(t, srv.Reset(ctx))
assert.FileExists(t, leasesPath)
assert.Empty(t, srv.Leases())
}
func TestServer_Leases(t *testing.T) {
leasesPath := newTempDB(t)
conf := &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
srv, err := dhcpsvc.New(ctx, conf)
require.NoError(t, err)
expiry, err := time.Parse(time.RFC3339, "2042-01-02T03:04:05Z")
require.NoError(t, err)
wantLeases := []*dhcpsvc.Lease{{
Expiry: expiry,
IP: netip.MustParseAddr("192.168.0.3"),
Hostname: "example.host",
HWAddr: mustParseMAC(t, "AA:AA:AA:AA:AA:AA"),
IsStatic: false,
}, {
Expiry: time.Time{},
IP: netip.MustParseAddr("192.168.0.4"),
Hostname: "example.static.host",
HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"),
IsStatic: true,
}}
assert.ElementsMatch(t, wantLeases, srv.Leases())
}

View File

@@ -0,0 +1,19 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host3",
"mac": "06:05:04:03:02:01",
"static": true
}
],
"version": 1
}

View File

@@ -0,0 +1,33 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.168.0.3",
"hostname": "host2",
"mac": "06:05:04:03:02:01",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host3",
"mac": "02:03:04:05:06:07",
"static": true
},
{
"expires": "",
"ip": "2001:db8::3",
"hostname": "host4",
"mac": "06:05:04:03:02:02",
"static": true
}
],
"version": 1
}

View File

@@ -0,0 +1,26 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.168.0.3",
"hostname": "host2",
"mac": "01:02:03:04:05:07",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host4",
"mac": "06:05:04:03:02:02",
"static": true
}
],
"version": 1
}

View File

@@ -0,0 +1,33 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.168.0.3",
"hostname": "host2",
"mac": "06:05:04:03:02:01",
"static": true
},
{
"expires": "",
"ip": "172.16.0.3",
"hostname": "host3",
"mac": "02:03:04:05:06:07",
"static": true
},
{
"expires": "",
"ip": "172.16.0.4",
"hostname": "host4",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View File

@@ -0,0 +1,15 @@
{
"leases": [{
"expires": "2042-01-02T03:04:05Z",
"ip": "192.168.0.3",
"hostname": "example.host",
"mac": "AA:AA:AA:AA:AA:AA",
"static": false
}, {
"ip": "192.168.0.4",
"hostname": "example.static.host",
"mac": "BB:BB:BB:BB:BB:BB",
"static": true
}],
"version": 1
}

View File

@@ -82,8 +82,12 @@ func (c *IPv4Config) validate() (err error) {
return errors.Join(errs...)
}
// netInterfaceV4 is a DHCP interface for IPv4 address family.
type netInterfaceV4 struct {
// dhcpInterfaceV4 is a DHCP interface for IPv4 address family.
type dhcpInterfaceV4 struct {
// common is the common part of any network interface within the DHCP
// server.
common *netInterface
// gateway is the IP address of the network gateway.
gateway netip.Addr
@@ -101,25 +105,22 @@ type netInterfaceV4 struct {
// explicitOpts are the user-configured options. It must not have
// intersections with implicitOpts.
explicitOpts layers.DHCPOptions
// netInterface is embedded here to provide some common network interface
// logic.
netInterface
}
// newNetInterfaceV4 creates a new DHCP interface for IPv4 address family with
// newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with
// the given configuration. It returns an error if the given configuration
// can't be used.
func newNetInterfaceV4(
func newDHCPInterfaceV4(
ctx context.Context,
l *slog.Logger,
name string,
conf *IPv4Config,
) (i *netInterfaceV4, err error) {
) (i *dhcpInterfaceV4, err error) {
l = l.With(
keyInterface, name,
keyFamily, netutil.AddrFamilyIPv4,
)
if !conf.Enabled {
l.DebugContext(ctx, "disabled")
@@ -143,35 +144,31 @@ func newNetInterfaceV4(
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
}
i = &netInterfaceV4{
i = &dhcpInterfaceV4{
gateway: conf.GatewayIP,
subnet: subnet,
addrSpace: addrSpace,
netInterface: netInterface{
name: name,
leaseTTL: conf.LeaseDuration,
logger: l,
},
common: newNetInterface(name, l, conf.LeaseDuration),
}
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
return i, nil
}
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
type netInterfacesV4 []*netInterfaceV4
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
type dhcpInterfacesV4 []*dhcpInterfaceV4
// find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface.
func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) {
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
return iface.subnet.Contains(ip)
})
if i < 0 {
return nil, false
}
return &ifaces[i].netInterface, true
return ifaces[i].common, true
}
// options returns the implicit and explicit options for the interface. The two

View File

@@ -62,10 +62,12 @@ func (c *IPv6Config) validate() (err error) {
return errors.Join(errs...)
}
// netInterfaceV6 is a DHCP interface for IPv6 address family.
//
// TODO(e.burkov): Add options.
type netInterfaceV6 struct {
// dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
type dhcpInterfaceV6 struct {
// common is the common part of any network interface within the DHCP
// server.
common *netInterface
// rangeStart is the first IP address in the range.
rangeStart netip.Addr
@@ -78,10 +80,6 @@ type netInterfaceV6 struct {
// intersections with implicitOpts.
explicitOpts layers.DHCPv6Options
// netInterface is embedded here to provide some common network interface
// logic.
netInterface
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
// flags.
raSLAACOnly bool
@@ -90,16 +88,16 @@ type netInterfaceV6 struct {
raAllowSLAAC bool
}
// newNetInterfaceV6 creates a new DHCP interface for IPv6 address family with
// newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with
// the given configuration.
//
// TODO(e.burkov): Validate properly.
func newNetInterfaceV6(
func newDHCPInterfaceV6(
ctx context.Context,
l *slog.Logger,
name string,
conf *IPv6Config,
) (i *netInterfaceV6) {
) (i *dhcpInterfaceV6) {
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
if !conf.Enabled {
l.DebugContext(ctx, "disabled")
@@ -107,13 +105,9 @@ func newNetInterfaceV6(
return nil
}
i = &netInterfaceV6{
rangeStart: conf.RangeStart,
netInterface: netInterface{
name: name,
leaseTTL: conf.LeaseDuration,
logger: l,
},
i = &dhcpInterfaceV6{
rangeStart: conf.RangeStart,
common: newNetInterface(name, l, conf.LeaseDuration),
raSLAACOnly: conf.RASLAACOnly,
raAllowSLAAC: conf.RAAllowSLAAC,
}
@@ -122,12 +116,12 @@ func newNetInterfaceV6(
return i
}
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
type netInterfacesV6 []*netInterfaceV6
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
type dhcpInterfacesV6 []*dhcpInterfaceV6
// find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface.
func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
func (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
// prefLen is the length of prefix to match ip against.
//
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
@@ -136,7 +130,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
// be used instead.
const prefLen = netutil.IPv6BitLen - 8
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) {
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {
return !ip.Less(iface.rangeStart) &&
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
})
@@ -144,7 +138,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
return nil, false
}
return &ifaces[i].netInterface, true
return ifaces[i].common, true
}
// options returns the implicit and explicit options for the interface. The two

View File

@@ -8,6 +8,7 @@ import (
"testing"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/quic-go/quic-go"
"github.com/stretchr/testify/assert"
@@ -217,7 +218,8 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
}
srv := &Server{
conf: ServerConfig{TLSConfig: tlsConf},
conf: ServerConfig{TLSConfig: tlsConf},
baseLogger: slogutil.NewDiscardLogger(),
}
var (

View File

@@ -22,6 +22,7 @@ import (
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -158,7 +159,7 @@ type Config struct {
// IpsetList is the ipset configuration that allows AdGuard Home to add IP
// addresses of the specified domain names to an ipset list. Syntax:
//
// DOMAIN[,DOMAIN].../IPSET_NAME
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
//
// This field is ignored if [IpsetListFileName] is set.
IpsetList []string `yaml:"ipset"`
@@ -301,6 +302,8 @@ type ServerConfig struct {
// UpstreamMode is a enumeration of upstream mode representations. See
// [proxy.UpstreamModeType].
//
// TODO(d.kolyshev): Consider using [proxy.UpstreamMode].
type UpstreamMode string
const (
@@ -315,6 +318,7 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)
conf = &proxy.Config{
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
HTTP3: srvConf.ServeHTTP3,
Ratelimit: int(srvConf.Ratelimit),
RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
@@ -420,8 +424,6 @@ func parseBogusNXDOMAIN(confBogusNXDOMAIN []string) (subnets []netip.Prefix, err
return subnets, nil
}
const defaultBlockedResponseTTL = 3600
// initDefaultSettings initializes default settings if nothing
// is configured
func (s *Server) initDefaultSettings() {
@@ -452,24 +454,24 @@ func (s *Server) initDefaultSettings() {
// prepareIpsetListSettings reads and prepares the ipset configuration either
// from a file or from the data in the configuration file.
func (s *Server) prepareIpsetListSettings() (err error) {
func (s *Server) prepareIpsetListSettings() (ipsets []string, err error) {
fn := s.conf.IpsetListFileName
if fn == "" {
return s.ipset.init(s.conf.IpsetList)
return s.conf.IpsetList, nil
}
// #nosec G304 -- Trust the path explicitly given by the user.
data, err := os.ReadFile(fn)
if err != nil {
return err
return nil, err
}
ipsets := stringutil.SplitTrimmed(string(data), "\n")
ipsets = stringutil.FilterOut(ipsets, IsCommentOrEmpty)
ipsets = stringutil.SplitTrimmed(string(data), "\n")
ipsets = slices.DeleteFunc(ipsets, IsCommentOrEmpty)
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
return s.ipset.init(ipsets)
return ipsets, nil
}
// loadUpstreams parses upstream DNS servers from the configured file or from
@@ -690,7 +692,7 @@ func matchesDomainWildcard(host, pat string) (ok bool) {
// the DNS names and patterns from certificate. dnsNames must be sorted.
func anyNameMatches(dnsNames []string, sni string) (ok bool) {
// Check sni is either a valid hostname or a valid IP address.
if netutil.ValidateHostname(sni) != nil && net.ParseIP(sni) == nil {
if !netutil.IsValidHostname(sni) && !netutil.IsValidIPString(sni) {
return false
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
)
// DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames.
@@ -28,7 +29,7 @@ func (s *Server) DialContext(ctx context.Context, network, addr string) (conn ne
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil {
if netutil.IsValidIPString(host) {
return dialer.DialContext(ctx, network, addr)
}

View File

@@ -6,6 +6,7 @@ import (
"context"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"net/netip"
@@ -27,6 +28,7 @@ import (
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/sysresolv"
"github.com/AdguardTeam/golibs/stringutil"
@@ -121,12 +123,17 @@ type Server struct {
// access drops disallowed clients.
access *accessManager
// baseLogger is used to create loggers for other entities. It should not
// have a prefix and must not be nil.
baseLogger *slog.Logger
// localDomainSuffix is the suffix used to detect internal hosts. It
// must be a valid domain name plus dots on each side.
localDomainSuffix string
// ipset processes DNS requests using ipset data.
ipset ipsetCtx
// ipset processes DNS requests using ipset data. It must not be nil after
// initialization. See [newIpsetHandler].
ipset *ipsetHandler
// privateNets is the configured set of IP networks considered private.
privateNets netutil.SubnetSet
@@ -197,6 +204,10 @@ type DNSCreateParams struct {
PrivateNets netutil.SubnetSet
Anonymizer *aghnet.IPMut
EtcHosts *aghnet.HostsContainer
// Logger is used as a base logger. It must not be nil.
Logger *slog.Logger
LocalDomain string
}
@@ -233,6 +244,7 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
stats: p.Stats,
queryLog: p.QueryLog,
privateNets: p.PrivateNets,
baseLogger: p.Logger,
// TODO(e.burkov): Use some case-insensitive string comparison.
localDomainSuffix: strings.ToLower(localDomainSuffix),
etcHosts: etcHosts,
@@ -596,11 +608,18 @@ func (s *Server) prepareLocalResolvers() (uc *proxy.UpstreamConfig, err error) {
// the primary DNS proxy instance. It assumes s.serverLock is locked or the
// Server not running.
func (s *Server) prepareInternalDNS() (err error) {
err = s.prepareIpsetListSettings()
ipsetList, err := s.prepareIpsetListSettings()
if err != nil {
return fmt.Errorf("preparing ipset settings: %w", err)
}
ipsetLogger := s.baseLogger.With(slogutil.KeyPrefix, "ipset")
s.ipset, err = newIpsetHandler(context.TODO(), ipsetLogger, ipsetList)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
bootOpts := &upstream.Options{
Timeout: DefaultTimeout,
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
@@ -664,6 +683,7 @@ func (s *Server) setupAddrProc() {
s.addrProc = client.EmptyAddrProc{}
} else {
c := s.conf.AddrProcConf
c.BaseLogger = s.baseLogger
c.DialContext = s.DialContext
c.PrivateSubnets = s.privateNets
c.UsePrivateRDNS = s.conf.UsePrivateRDNS
@@ -707,6 +727,7 @@ func validateBlockingMode(
func (s *Server) prepareInternalProxy() (err error) {
srvConf := s.conf
conf := &proxy.Config{
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
CacheEnabled: true,
CacheSizeBytes: 4096,
PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,

View File

@@ -28,6 +28,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -99,6 +100,7 @@ func createTestServer(
DHCPServer: dhcp,
DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
@@ -339,7 +341,10 @@ func TestServer_timeout(t *testing.T) {
ServePlainDNS: true,
}
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)})
s, err := NewServer(DNSCreateParams{
DNSFilter: createTestDNSFilter(t),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
err = s.Prepare(srvConf)
@@ -349,7 +354,10 @@ func TestServer_timeout(t *testing.T) {
})
t.Run("default", func(t *testing.T) {
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)})
s, err := NewServer(DNSCreateParams{
DNSFilter: createTestDNSFilter(t),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
@@ -376,7 +384,9 @@ func TestServer_Prepare_fallbacks(t *testing.T) {
ServePlainDNS: true,
}
s, err := NewServer(DNSCreateParams{})
s, err := NewServer(DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
err = s.Prepare(srvConf)
@@ -962,6 +972,7 @@ func TestBlockedCustomIP(t *testing.T) {
DHCPServer: dhcp,
DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
@@ -1127,6 +1138,7 @@ func TestRewrite(t *testing.T) {
DHCPServer: dhcp,
DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
@@ -1256,6 +1268,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
},
},
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
LocalDomain: localDomain,
})
require.NoError(t, err)
@@ -1341,6 +1354,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
DHCPServer: dhcp,
DNSFilter: flt,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
@@ -1392,24 +1406,29 @@ func TestNewServer(t *testing.T) {
in DNSCreateParams
wantErrMsg string
}{{
name: "success",
in: DNSCreateParams{},
name: "success",
in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
},
wantErrMsg: "",
}, {
name: "success_local_tld",
in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
LocalDomain: "mynet",
},
wantErrMsg: "",
}, {
name: "success_local_domain",
in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
LocalDomain: "my.local.net",
},
wantErrMsg: "",
}, {
name: "bad_local_domain",
in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
LocalDomain: "!!!",
},
wantErrMsg: `local domain: bad domain name "!!!": ` +

View File

@@ -9,6 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
@@ -57,6 +58,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
},
DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)
@@ -229,6 +231,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
DHCPServer: &testDHCP{},
DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err)

View File

@@ -228,7 +228,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
}, {
name: "upstream_dns_bad",
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
`cannot prepare the upstream: invalid address !!!: bad hostname "!!!": ` +
`cannot prepare the upstream: invalid address !!!: bad domain name "!!!": ` +
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
}, {
name: "bootstraps_bad",

View File

@@ -1,28 +1,43 @@
package dnsforward
import (
"context"
"fmt"
"log/slog"
"net"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/ipset"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/miekg/dns"
)
// ipsetCtx is the ipset context. ipsetMgr can be nil.
type ipsetCtx struct {
// ipsetHandler is the ipset context. ipsetMgr can be nil.
type ipsetHandler struct {
ipsetMgr ipset.Manager
logger *slog.Logger
}
// init initializes the ipset context. It is not safe for concurrent use.
//
// TODO(a.garipov): Rewrite into a simple constructor?
func (c *ipsetCtx) init(ipsetConf []string) (err error) {
c.ipsetMgr, err = ipset.NewManager(ipsetConf)
if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrPermission) {
// newIpsetHandler returns a new initialized [ipsetHandler]. It is not safe for
// concurrent use.
func newIpsetHandler(
ctx context.Context,
logger *slog.Logger,
ipsetList []string,
) (h *ipsetHandler, err error) {
h = &ipsetHandler{
logger: logger,
}
conf := &ipset.Config{
Logger: logger,
Lines: ipsetList,
}
h.ipsetMgr, err = ipset.NewManager(ctx, conf)
if errors.Is(err, os.ErrInvalid) ||
errors.Is(err, os.ErrPermission) ||
errors.Is(err, errors.ErrUnsupported) {
// ipset cannot currently be initialized if the server was installed
// from Snap or when the user or the binary doesn't have the required
// permissions, or when the kernel doesn't support netfilter.
@@ -31,30 +46,28 @@ func (c *ipsetCtx) init(ipsetConf []string) (err error) {
//
// TODO(a.garipov): The Snap problem can probably be solved if we add
// the netlink-connector interface plug.
log.Info("ipset: warning: cannot initialize: %s", err)
logger.WarnContext(ctx, "cannot initialize", slogutil.KeyError, err)
return nil
} else if errors.Is(err, errors.ErrUnsupported) {
log.Info("ipset: warning: %s", err)
return nil
return h, nil
} else if err != nil {
return fmt.Errorf("initializing ipset: %w", err)
return nil, fmt.Errorf("initializing ipset: %w", err)
}
return h, nil
}
// close closes the Linux Netfilter connections. close can be called on a nil
// handler.
func (h *ipsetHandler) close() (err error) {
if h != nil && h.ipsetMgr != nil {
return h.ipsetMgr.Close()
}
return nil
}
// close closes the Linux Netfilter connections.
func (c *ipsetCtx) close() (err error) {
if c.ipsetMgr != nil {
return c.ipsetMgr.Close()
}
return nil
}
func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
// dctxIsFilled returns true if dctx has enough information to process.
func dctxIsFilled(dctx *dnsContext) (ok bool) {
return dctx != nil &&
dctx.responseFromUpstream &&
dctx.proxyCtx != nil &&
@@ -65,8 +78,8 @@ func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
// skipIpsetProcessing returns true when the ipset processing can be skipped for
// this request.
func (c *ipsetCtx) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
if c == nil || c.ipsetMgr == nil || !c.dctxIsfilled(dctx) {
func (h *ipsetHandler) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
if h == nil || h.ipsetMgr == nil || !dctxIsFilled(dctx) {
return true
}
@@ -108,31 +121,31 @@ func ipsFromAnswer(ans []dns.RR) (ip4s, ip6s []net.IP) {
}
// process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(dctx *dnsContext) (rc resultCode) {
log.Debug("dnsforward: ipset: started processing")
defer log.Debug("dnsforward: ipset: finished processing")
func (h *ipsetHandler) process(dctx *dnsContext) (rc resultCode) {
// TODO(s.chzhen): Use passed context.
ctx := context.TODO()
h.logger.DebugContext(ctx, "started processing")
defer h.logger.DebugContext(ctx, "finished processing")
if c.skipIpsetProcessing(dctx) {
if h.skipIpsetProcessing(dctx) {
return resultCodeSuccess
}
log.Debug("ipset: starting processing")
req := dctx.proxyCtx.Req
host := req.Question[0].Name
host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host)
ip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer)
n, err := c.ipsetMgr.Add(host, ip4s, ip6s)
n, err := h.ipsetMgr.Add(ctx, host, ip4s, ip6s)
if err != nil {
// Consider ipset errors non-critical to the request.
log.Error("dnsforward: ipset: adding host ips: %s", err)
h.logger.ErrorContext(ctx, "adding host ips", slogutil.KeyError, err)
return resultCodeSuccess
}
log.Debug("dnsforward: ipset: added %d new ipset entries", n)
h.logger.DebugContext(ctx, "added new ipset entries", "num", n)
return resultCodeSuccess
}

View File

@@ -1,10 +1,12 @@
package dnsforward
import (
"context"
"net"
"testing"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
@@ -16,7 +18,7 @@ type fakeIpsetMgr struct {
}
// Add implements the aghnet.IpsetManager interface for *fakeIpsetMgr.
func (m *fakeIpsetMgr) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
func (m *fakeIpsetMgr) Add(_ context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {
m.ip4s = append(m.ip4s, ip4s...)
m.ip6s = append(m.ip6s, ip6s...)
@@ -58,7 +60,9 @@ func TestIpsetCtx_process(t *testing.T) {
responseFromUpstream: true,
}
ictx := &ipsetCtx{}
ictx := &ipsetHandler{
logger: slogutil.NewDiscardLogger(),
}
rc := ictx.process(dctx)
assert.Equal(t, resultCodeSuccess, rc)
@@ -77,8 +81,9 @@ func TestIpsetCtx_process(t *testing.T) {
}
m := &fakeIpsetMgr{}
ictx := &ipsetCtx{
ictx := &ipsetHandler{
ipsetMgr: m,
logger: slogutil.NewDiscardLogger(),
}
rc := ictx.process(dctx)
@@ -101,8 +106,9 @@ func TestIpsetCtx_process(t *testing.T) {
}
m := &fakeIpsetMgr{}
ictx := &ipsetCtx{
ictx := &ipsetHandler{
ipsetMgr: m,
logger: slogutil.NewDiscardLogger(),
}
rc := ictx.process(dctx)
@@ -124,8 +130,9 @@ func TestIpsetCtx_SkipIpsetProcessing(t *testing.T) {
}
m := &fakeIpsetMgr{}
ictx := &ipsetCtx{
ictx := &ipsetHandler{
ipsetMgr: m,
logger: slogutil.NewDiscardLogger(),
}
testCases := []struct {

View File

@@ -58,7 +58,7 @@ func (s *Server) genDNSFilterMessage(
return s.replyCompressed(req)
}
return s.newMsgNODATA(req)
return s.NewMsgNODATA(req)
}
switch res.Reason {
@@ -344,51 +344,6 @@ func (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {
return s.reply(req, dns.RcodeRefused)
}
// newMsgNODATA returns a properly initialized NODATA response.
//
// See https://www.rfc-editor.org/rfc/rfc2308#section-2.2.
func (s *Server) newMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeSuccess)
resp.Ns = s.genSOA(req)
return resp
}
func (s *Server) genSOA(request *dns.Msg) []dns.RR {
zone := ""
if len(request.Question) > 0 {
zone = request.Question[0].Name
}
soa := dns.SOA{
// values copied from verisign's nonexistent .com domain
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: s.dnsFilter.BlockedResponseTTL(),
Class: dns.ClassINET,
},
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultBlockedResponseTTL
}
if len(zone) > 0 && zone[0] != '.' {
soa.Mbox += zone
}
return []dns.RR{&soa}
}
// type check
var _ proxy.MessageConstructor = (*Server)(nil)
@@ -425,3 +380,52 @@ func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {
return resp
}
// NewMsgNODATA implements the [proxy.MessageConstructor] interface for *Server.
func (s *Server) NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeSuccess)
resp.Ns = s.genSOA(req)
return resp
}
func (s *Server) genSOA(req *dns.Msg) []dns.RR {
zone := ""
if len(req.Question) > 0 {
zone = req.Question[0].Name
}
const defaultBlockedResponseTTL = 3600
soa := dns.SOA{
// Values copied from verisign's nonexistent.com domain.
//
// Their exact values are not important in our use case because they are
// used for domain transfers between primary/secondary DNS servers.
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: s.dnsFilter.BlockedResponseTTL(),
Class: dns.ClassINET,
},
// zone will be appended later if it's not ".".
Mbox: "hostmaster.",
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultBlockedResponseTTL
}
if zone != "." {
soa.Mbox += zone
}
return []dns.RR{&soa}
}

View File

@@ -159,7 +159,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
q := pctx.Req.Question[0]
qt := q.Qtype
if s.conf.AAAADisabled && qt == dns.TypeAAAA {
_ = proxy.CheckDisabledAAAARequest(pctx, true)
pctx.Res = s.NewMsgNODATA(pctx.Req)
return resultCodeFinish
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter/rules"
@@ -430,6 +431,7 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
dnsFilter: createTestDNSFilter(t),
dhcpServer: dhcp,
localDomainSuffix: localDomainSuffix,
baseLogger: slogutil.NewDiscardLogger(),
}
req := &dns.Msg{
@@ -565,6 +567,7 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
dnsFilter: createTestDNSFilter(t),
dhcpServer: testDHCP,
localDomainSuffix: tc.suffix,
baseLogger: slogutil.NewDiscardLogger(),
}
req := &dns.Msg{

View File

@@ -11,6 +11,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -202,6 +203,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
ql := &testQueryLog{}
st := &testStats{}
srv := &Server{
baseLogger: slogutil.NewDiscardLogger(),
queryLog: ql,
stats: st,
anonymizer: aghnet.NewIPMut(nil),

View File

@@ -150,12 +150,12 @@ func setProxyUpstreamMode(
) (err error) {
switch upstreamMode {
case UpstreamModeParallel:
conf.UpstreamMode = proxy.UModeParallel
conf.UpstreamMode = proxy.UpstreamModeParallel
case UpstreamModeFastestAddr:
conf.UpstreamMode = proxy.UModeFastestAddr
conf.UpstreamMode = proxy.UpstreamModeFastestAddr
conf.FastestPingTimeout = fastestTimeout
case UpstreamModeLoadBalance:
conf.UpstreamMode = proxy.UModeLoadBalance
conf.UpstreamMode = proxy.UpstreamModeLoadBalance
default:
return fmt.Errorf("unexpected value %q", upstreamMode)
}

View File

@@ -47,7 +47,6 @@ func fromCacheItem(item *cacheItem) (data []byte) {
data = binary.BigEndian.AppendUint64(data, uint64(expiry))
for _, v := range item.hashes {
// nolint:looppointer // The subslice of v is used for a copy.
data = append(data, v[:]...)
}
@@ -63,7 +62,6 @@ func (c *Checker) findInCache(
i := 0
for _, hash := range hashes {
// nolint:looppointer // The has subslice is used for a cache lookup.
data := c.cache.Get(hash[:prefixLen])
if data == nil {
hashes[i] = hash
@@ -98,7 +96,6 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
for _, hash := range respHashes {
var pref prefix
// nolint:looppointer // The hash subslice is used for a copy.
copy(pref[:], hash[:])
hashToStore[pref] = append(hashToStore[pref], hash)
@@ -109,11 +106,9 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
}
for _, hash := range hashesToRequest {
// nolint:looppointer // The hash subslice is used for a cache lookup.
val := c.cache.Get(hash[:prefixLen])
if val == nil {
var pref prefix
// nolint:looppointer // The hash subslice is used for a copy.
copy(pref[:], hash[:])
c.setCache(pref, nil)

View File

@@ -173,7 +173,6 @@ func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
b := &strings.Builder{}
for _, hash := range hashes {
// nolint:looppointer // The hash subslice is used for hex encoding.
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
}

View File

@@ -22,6 +22,7 @@ type SafeSearchConfig struct {
Bing bool `yaml:"bing" json:"bing"`
DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"`
Ecosia bool `yaml:"ecosia" json:"ecosia"`
Google bool `yaml:"google" json:"google"`
Pixabay bool `yaml:"pixabay" json:"pixabay"`
Yandex bool `yaml:"yandex" json:"yandex"`

View File

@@ -14,6 +14,9 @@ var pixabay string
//go:embed rules/duckduckgo.txt
var duckduckgo string
//go:embed rules/ecosia.txt
var ecosia string
//go:embed rules/yandex.txt
var yandex string
@@ -27,6 +30,7 @@ var youtube string
var safeSearchRules = map[Service]string{
Bing: bing,
DuckDuckGo: duckduckgo,
Ecosia: ecosia,
Google: google,
Pixabay: pixabay,
Yandex: yandex,

View File

@@ -1 +1,2 @@
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|edgeservices.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com

View File

@@ -0,0 +1 @@
|www.ecosia.org^$dnsrewrite=NOERROR;CNAME;strict-safe-search.ecosia.org

View File

@@ -46,6 +46,9 @@
|www.google.co.uz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.co.ve^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.co.vi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.co.za^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.co.zm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.co.zw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.com.af^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.com.ag^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|www.google.com.ai^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com

View File

@@ -28,6 +28,7 @@ type Service string
const (
Bing Service = "bing"
DuckDuckGo Service = "duckduckgo"
Ecosia Service = "ecosia"
Google Service = "google"
Pixabay Service = "pixabay"
Yandex Service = "yandex"
@@ -41,6 +42,8 @@ func isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool)
return s.Bing
case DuckDuckGo:
return s.DuckDuckGo
case Ecosia:
return s.Ecosia
case Google:
return s.Google
case Pixabay:

View File

@@ -25,6 +25,7 @@ var defaultSafeSearchConf = filtering.SafeSearchConfig{
Enabled: true,
Bing: true,
DuckDuckGo: true,
Ecosia: true,
Google: true,
Pixabay: true,
Yandex: true,

View File

@@ -34,6 +34,7 @@ var testConf = filtering.SafeSearchConfig{
Bing: true,
DuckDuckGo: true,
Ecosia: true,
Google: true,
Pixabay: true,
Yandex: true,

File diff suppressed because one or more lines are too long

View File

@@ -97,6 +97,7 @@ func glGetTokenDate(file string) uint32 {
buf := bytes.NewBuffer(bs)
// TODO(a.garipov): Get rid of github.com/josharian/native dependency.
err = binary.Read(buf, native.Endian, &dateToken)
if err != nil {
log.Error("decoding token: %s", err)

View File

@@ -1,8 +1,8 @@
package home
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"sync"
@@ -11,7 +11,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
@@ -20,50 +19,18 @@ import (
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
)
// DHCP is an interface for accessing DHCP lease data the [clientsContainer]
// needs.
type DHCP interface {
// Leases returns all the DHCP leases.
Leases() (leases []*dhcpsvc.Lease)
// HostByIP returns the hostname of the DHCP client with the given IP
// address. The address will be netip.Addr{} if there is no such client,
// due to an assumption that a DHCP client must always have a hostname.
HostByIP(ip netip.Addr) (host string)
// MACByIP returns the MAC address for the given IP address leased. It
// returns nil if there is no such client, due to an assumption that a DHCP
// client must always have a MAC address.
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
}
// clientsContainer is the storage of all runtime and persistent clients.
type clientsContainer struct {
// storage stores information about persistent clients.
storage *client.Storage
// runtimeIndex stores information about runtime clients.
runtimeIndex *client.RuntimeIndex
// dhcp is the DHCP service implementation.
dhcp DHCP
// clientChecker checks if a client is blocked by the current access
// settings.
clientChecker BlockedClientChecker
// etcHosts contains list of rewrite rules taken from the operating system's
// hosts database.
etcHosts *aghnet.HostsContainer
// arpDB stores the neighbors retrieved from ARP.
arpDB arpdb.Interface
// lock protects all fields.
//
// TODO(a.garipov): Use a pointer and describe which fields are protected in
@@ -95,7 +62,7 @@ type BlockedClientChecker interface {
// Note: this function must be called only once
func (clients *clientsContainer) Init(
objects []*clientObject,
dhcpServer DHCP,
dhcpServer client.DHCP,
etcHosts *aghnet.HostsContainer,
arpDB arpdb.Interface,
filteringConf *filtering.Config,
@@ -105,28 +72,15 @@ func (clients *clientsContainer) Init(
return errors.Error("clients container already initialized")
}
clients.runtimeIndex = client.NewRuntimeIndex()
confClients := make([]*client.Persistent, 0, len(objects))
for i, o := range objects {
var p *client.Persistent
p, err = o.toPersistent(filteringConf)
if err != nil {
return fmt.Errorf("init persistent client at index %d: %w", i, err)
}
clients.storage = client.NewStorage(&client.Config{
AllowedTags: clientTags,
})
// TODO(e.burkov): Use [dhcpsvc] implementation when it's ready.
clients.dhcp = dhcpServer
clients.etcHosts = etcHosts
clients.arpDB = arpDB
err = clients.addFromConfig(objects, filteringConf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize
clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)
if clients.testing {
return nil
confClients = append(confClients, p)
}
// The clients.etcHosts may be nil even if config.Clients.Sources.HostsFile
@@ -135,21 +89,26 @@ func (clients *clientsContainer) Init(
// TODO(e.burkov): The option should probably be returned, since hosts file
// currently used not only for clients' information enrichment, but also in
// the filtering module and upstream addresses resolution.
if config.Clients.Sources.HostsFile && clients.etcHosts != nil {
go clients.handleHostsUpdates()
var hosts client.HostsContainer = etcHosts
if !config.Clients.Sources.HostsFile {
hosts = nil
}
clients.storage, err = client.NewStorage(&client.StorageConfig{
InitialClients: confClients,
DHCP: dhcpServer,
EtcHosts: hosts,
ARPDB: arpDB,
ARPClientsUpdatePeriod: arpClientsUpdatePeriod,
RuntimeSourceDHCP: config.Clients.Sources.DHCP,
})
if err != nil {
return fmt.Errorf("init client storage: %w", err)
}
return nil
}
// handleHostsUpdates receives the updates from the hosts container and adds
// them to the clients container. It is intended to be used as a goroutine.
func (clients *clientsContainer) handleHostsUpdates() {
for upd := range clients.etcHosts.Upd() {
clients.addFromHostsFile(upd)
}
}
// webHandlersRegistered prevents a [clientsContainer] from registering its web
// handlers more than once.
//
@@ -157,7 +116,7 @@ func (clients *clientsContainer) handleHostsUpdates() {
var webHandlersRegistered = false
// Start starts the clients container.
func (clients *clientsContainer) Start() {
func (clients *clientsContainer) Start(ctx context.Context) (err error) {
if clients.testing {
return
}
@@ -167,14 +126,7 @@ func (clients *clientsContainer) Start() {
clients.registerWebHandlers()
}
go clients.periodicUpdate()
}
// reloadARP reloads runtime clients from ARP, if configured.
func (clients *clientsContainer) reloadARP() {
if clients.arpDB != nil {
clients.addFromSystemARP()
}
return clients.storage.Start(ctx)
}
// clientObject is the YAML representation of a persistent client.
@@ -275,28 +227,6 @@ func (o *clientObject) toPersistent(
return cli, nil
}
// addFromConfig initializes the clients container with objects from the
// configuration file.
func (clients *clientsContainer) addFromConfig(
objects []*clientObject,
filteringConf *filtering.Config,
) (err error) {
for i, o := range objects {
var cli *client.Persistent
cli, err = o.toPersistent(filteringConf)
if err != nil {
return fmt.Errorf("clients: init persistent client at index %d: %w", i, err)
}
err = clients.storage.Add(cli)
if err != nil {
return fmt.Errorf("adding client %q at index %d: %w", cli.Name, i, err)
}
}
return nil
}
// forConfig returns all currently known persistent clients as objects for the
// configuration file.
func (clients *clientsContainer) forConfig() (objs []*clientObject) {
@@ -337,39 +267,6 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
// arpClientsUpdatePeriod defines how often ARP clients are updated.
const arpClientsUpdatePeriod = 10 * time.Minute
func (clients *clientsContainer) periodicUpdate() {
defer log.OnPanic("clients container")
for {
clients.reloadARP()
time.Sleep(arpClientsUpdatePeriod)
}
}
// clientSource checks if client with this IP address already exists and returns
// the source which updated it last. It returns [client.SourceNone] if the
// client doesn't exist. Note that it is only used in tests.
func (clients *clientsContainer) clientSource(ip netip.Addr) (src client.Source) {
clients.lock.Lock()
defer clients.lock.Unlock()
_, ok := clients.findLocked(ip.String())
if ok {
return client.SourcePersistent
}
rc := clients.runtimeIndex.Client(ip)
if rc != nil {
src, _ = rc.Info()
}
if src < client.SourceDHCP && clients.dhcp.HostByIP(ip) != "" {
src = client.SourceDHCP
}
return src
}
// findMultiple is a wrapper around [clientsContainer.find] to make it a valid
// client finder for the query log. c is never nil; if no information about the
// client is found, it returns an artificial client record by only setting the
@@ -415,7 +312,7 @@ func (clients *clientsContainer) clientOrArtificial(
}, false
}
rc := clients.findRuntimeClient(ip)
rc := clients.storage.ClientRuntime(ip)
if rc != nil {
_, host := rc.Info()
@@ -430,19 +327,6 @@ func (clients *clientsContainer) clientOrArtificial(
}, true
}
// find returns a shallow copy of the client if there is one found.
func (clients *clientsContainer) find(id string) (c *client.Persistent, ok bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok = clients.findLocked(id)
if !ok {
return nil, false
}
return c, true
}
// shouldCountClient is a wrapper around [clientsContainer.find] to make it a
// valid client information finder for the statistics. If no information about
// the client is found, it returns true.
@@ -451,7 +335,7 @@ func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
defer clients.lock.Unlock()
for _, id := range ids {
client, ok := clients.findLocked(id)
client, ok := clients.storage.Find(id)
if ok {
return !client.IgnoreStatistics
}
@@ -473,7 +357,7 @@ func (clients *clientsContainer) UpstreamConfigByID(
clients.lock.Lock()
defer clients.lock.Unlock()
c, ok := clients.findLocked(id)
c, ok := clients.storage.Find(id)
if !ok {
return nil, nil
} else if c.UpstreamConfig != nil {
@@ -511,225 +395,17 @@ func (clients *clientsContainer) UpstreamConfigByID(
return conf, nil
}
// findLocked searches for a client by its ID. clients.lock is expected to be
// locked.
func (clients *clientsContainer) findLocked(id string) (c *client.Persistent, ok bool) {
c, ok = clients.storage.Find(id)
if ok {
return c, true
}
ip, err := netip.ParseAddr(id)
if err != nil {
return nil, false
}
// TODO(e.burkov): Iterate through clients.list only once.
return clients.findDHCP(ip)
}
// findDHCP searches for a client by its MAC, if the DHCP server is active and
// there is such client. clients.lock is expected to be locked.
func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *client.Persistent, ok bool) {
foundMAC := clients.dhcp.MACByIP(ip)
if foundMAC == nil {
return nil, false
}
return clients.storage.FindByMAC(foundMAC)
}
// runtimeClient returns a runtime client from internal index. Note that it
// doesn't include DHCP clients.
func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *client.Runtime) {
if ip == (netip.Addr{}) {
return nil
}
clients.lock.Lock()
defer clients.lock.Unlock()
return clients.runtimeIndex.Client(ip)
}
// findRuntimeClient finds a runtime client by their IP.
func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *client.Runtime) {
rc = clients.runtimeClient(ip)
host := clients.dhcp.HostByIP(ip)
if host != "" {
if rc == nil {
rc = client.NewRuntime(ip)
}
rc.SetInfo(client.SourceDHCP, []string{host})
return rc
}
return rc
}
// setWHOISInfo sets the WHOIS information for a client. clients.lock is
// expected to be locked.
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
_, ok := clients.findLocked(ip.String())
if ok {
log.Debug("clients: client for %s is already created, ignore whois info", ip)
return
}
rc := clients.runtimeIndex.Client(ip)
if rc == nil {
// Create a RuntimeClient implicitly so that we don't do this check
// again.
rc = client.NewRuntime(ip)
clients.runtimeIndex.Add(rc)
log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi)
} else {
host, _ := rc.Info()
log.Debug("clients: set whois info for runtime client %s: %+v", host, wi)
}
rc.SetWHOIS(wi)
}
// addHost adds a new IP-hostname pairing. The priorities of the sources are
// taken into account. ok is true if the pairing was added.
//
// TODO(a.garipov): Only used in internal tests. Consider removing.
func (clients *clientsContainer) addHost(
ip netip.Addr,
host string,
src client.Source,
) (ok bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
return clients.addHostLocked(ip, host, src)
}
// type check
var _ client.AddressUpdater = (*clientsContainer)(nil)
// UpdateAddress implements the [client.AddressUpdater] interface for
// *clientsContainer
func (clients *clientsContainer) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
// Common fast path optimization.
if host == "" && info == nil {
return
}
clients.lock.Lock()
defer clients.lock.Unlock()
if host != "" {
ok := clients.addHostLocked(ip, host, client.SourceRDNS)
if !ok {
log.Debug("clients: host for client %q already set with higher priority source", ip)
}
}
if info != nil {
clients.setWHOISInfo(ip, info)
}
}
// addHostLocked adds a new IP-hostname pairing. clients.lock is expected to be
// locked.
func (clients *clientsContainer) addHostLocked(
ip netip.Addr,
host string,
src client.Source,
) (ok bool) {
rc := clients.runtimeIndex.Client(ip)
if rc == nil {
if src < client.SourceDHCP {
if clients.dhcp.HostByIP(ip) != "" {
return false
}
}
rc = client.NewRuntime(ip)
clients.runtimeIndex.Add(rc)
}
rc.SetInfo(src, []string{host})
log.Debug(
"clients: adding client info %s -> %q %q [%d]",
ip,
src,
host,
clients.runtimeIndex.Size(),
)
return true
}
// addFromHostsFile fills the client-hostname pairing index from the system's
// hosts files.
func (clients *clientsContainer) addFromHostsFile(hosts *hostsfile.DefaultStorage) {
clients.lock.Lock()
defer clients.lock.Unlock()
deleted := clients.runtimeIndex.DeleteBySource(client.SourceHostsFile)
log.Debug("clients: removed %d client aliases from system hosts file", deleted)
added := 0
hosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {
// Only the first name of the first record is considered a canonical
// hostname for the IP address.
//
// TODO(e.burkov): Consider using all the names from all the records.
if clients.addHostLocked(addr, names[0], client.SourceHostsFile) {
added++
}
return true
})
log.Debug("clients: added %d client aliases from system hosts file", added)
}
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
// command.
func (clients *clientsContainer) addFromSystemARP() {
if err := clients.arpDB.Refresh(); err != nil {
log.Error("refreshing arp container: %s", err)
clients.arpDB = arpdb.Empty{}
return
}
ns := clients.arpDB.Neighbors()
if len(ns) == 0 {
log.Debug("refreshing arp container: the update is empty")
return
}
clients.lock.Lock()
defer clients.lock.Unlock()
deleted := clients.runtimeIndex.DeleteBySource(client.SourceARP)
log.Debug("clients: removed %d client aliases from arp neighborhood", deleted)
added := 0
for _, n := range ns {
if clients.addHostLocked(n.IP, n.Name, client.SourceARP) {
added++
}
}
log.Debug("clients: added %d client aliases from arp neighborhood", added)
clients.storage.UpdateAddress(ip, host, info)
}
// close gracefully closes all the client-specific upstream configurations of
// the persistent clients.
func (clients *clientsContainer) close() (err error) {
return clients.storage.CloseUpstreams()
func (clients *clientsContainer) close(ctx context.Context) (err error) {
return clients.storage.Shutdown(ctx)
}

View File

@@ -3,34 +3,14 @@ package home
import (
"net"
"net/netip"
"runtime"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testDHCP struct {
OnLeases func() (leases []*dhcpsvc.Lease)
OnHostBy func(ip netip.Addr) (host string)
OnMACBy func(ip netip.Addr) (mac net.HardwareAddr)
}
// Lease implements the [DHCP] interface for testDHCP.
func (t *testDHCP) Leases() (leases []*dhcpsvc.Lease) { return t.OnLeases() }
// HostByIP implements the [DHCP] interface for testDHCP.
func (t *testDHCP) HostByIP(ip netip.Addr) (host string) { return t.OnHostBy(ip) }
// MACByIP implements the [DHCP] interface for testDHCP.
func (t *testDHCP) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { return t.OnMACBy(ip) }
// newClientsContainer is a helper that creates a new clients container for
// tests.
func newClientsContainer(t *testing.T) (c *clientsContainer) {
@@ -40,316 +20,11 @@ func newClientsContainer(t *testing.T) (c *clientsContainer) {
testing: true,
}
dhcp := &testDHCP{
OnLeases: func() (leases []*dhcpsvc.Lease) { return nil },
OnHostBy: func(ip netip.Addr) (host string) { return "" },
OnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) { return nil },
}
require.NoError(t, c.Init(nil, dhcp, nil, nil, &filtering.Config{}))
require.NoError(t, c.Init(nil, client.EmptyDHCP{}, nil, nil, &filtering.Config{}))
return c
}
func TestClients(t *testing.T) {
clients := newClientsContainer(t)
t.Run("add_success", func(t *testing.T) {
var (
cliNone = "1.2.3.4"
cli1 = "1.1.1.1"
cli2 = "2.2.2.2"
cli1IP = netip.MustParseAddr(cli1)
cli2IP = netip.MustParseAddr(cli2)
cliIPv6 = netip.MustParseAddr("1:2:3::4")
)
c := &client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{cli1IP, cliIPv6},
}
err := clients.storage.Add(c)
require.NoError(t, err)
c = &client.Persistent{
Name: "client2",
UID: client.MustNewUID(),
IPs: []netip.Addr{cli2IP},
}
err = clients.storage.Add(c)
require.NoError(t, err)
c, ok := clients.find(cli1)
require.True(t, ok)
assert.Equal(t, "client1", c.Name)
c, ok = clients.find("1:2:3::4")
require.True(t, ok)
assert.Equal(t, "client1", c.Name)
c, ok = clients.find(cli2)
require.True(t, ok)
assert.Equal(t, "client2", c.Name)
_, ok = clients.find(cliNone)
assert.False(t, ok)
assert.Equal(t, clients.clientSource(cli1IP), client.SourcePersistent)
assert.Equal(t, clients.clientSource(cli2IP), client.SourcePersistent)
})
t.Run("add_fail_name", func(t *testing.T) {
err := clients.storage.Add(&client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("1.2.3.5")},
})
require.Error(t, err)
})
t.Run("add_fail_ip", func(t *testing.T) {
err := clients.storage.Add(&client.Persistent{
Name: "client3",
UID: client.MustNewUID(),
})
require.Error(t, err)
})
t.Run("update_fail_ip", func(t *testing.T) {
err := clients.storage.Update("client1", &client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
})
assert.Error(t, err)
})
t.Run("update_success", func(t *testing.T) {
var (
cliOld = "1.1.1.1"
cliNew = "1.1.1.2"
cliNewIP = netip.MustParseAddr(cliNew)
)
prev, ok := clients.storage.FindByName("client1")
require.True(t, ok)
require.NotNil(t, prev)
err := clients.storage.Update("client1", &client.Persistent{
Name: "client1",
UID: prev.UID,
IPs: []netip.Addr{cliNewIP},
})
require.NoError(t, err)
_, ok = clients.find(cliOld)
assert.False(t, ok)
assert.Equal(t, clients.clientSource(cliNewIP), client.SourcePersistent)
prev, ok = clients.storage.FindByName("client1")
require.True(t, ok)
require.NotNil(t, prev)
err = clients.storage.Update("client1", &client.Persistent{
Name: "client1-renamed",
UID: prev.UID,
IPs: []netip.Addr{cliNewIP},
UseOwnSettings: true,
})
require.NoError(t, err)
c, ok := clients.find(cliNew)
require.True(t, ok)
assert.Equal(t, "client1-renamed", c.Name)
assert.True(t, c.UseOwnSettings)
nilCli, ok := clients.storage.FindByName("client1")
require.False(t, ok)
assert.Nil(t, nilCli)
require.Len(t, c.IDs(), 1)
assert.Equal(t, cliNewIP, c.IPs[0])
})
t.Run("del_success", func(t *testing.T) {
ok := clients.storage.RemoveByName("client1-renamed")
require.True(t, ok)
_, ok = clients.find("1.1.1.2")
assert.False(t, ok)
})
t.Run("del_fail", func(t *testing.T) {
ok := clients.storage.RemoveByName("client3")
assert.False(t, ok)
})
t.Run("addhost_success", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
ok := clients.addHost(ip, "host", client.SourceARP)
assert.True(t, ok)
ok = clients.addHost(ip, "host2", client.SourceARP)
assert.True(t, ok)
ok = clients.addHost(ip, "host3", client.SourceHostsFile)
assert.True(t, ok)
assert.Equal(t, clients.clientSource(ip), client.SourceHostsFile)
})
t.Run("dhcp_replaces_arp", func(t *testing.T) {
ip := netip.MustParseAddr("1.2.3.4")
ok := clients.addHost(ip, "from_arp", client.SourceARP)
assert.True(t, ok)
assert.Equal(t, clients.clientSource(ip), client.SourceARP)
ok = clients.addHost(ip, "from_dhcp", client.SourceDHCP)
assert.True(t, ok)
assert.Equal(t, clients.clientSource(ip), client.SourceDHCP)
})
t.Run("addhost_priority", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
ok := clients.addHost(ip, "host1", client.SourceRDNS)
assert.True(t, ok)
assert.Equal(t, client.SourceHostsFile, clients.clientSource(ip))
})
}
func TestClientsWHOIS(t *testing.T) {
clients := newClientsContainer(t)
whois := &whois.Info{
Country: "AU",
Orgname: "Example Org",
}
t.Run("new_client", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.255")
clients.setWHOISInfo(ip, whois)
rc := clients.runtimeIndex.Client(ip)
require.NotNil(t, rc)
assert.Equal(t, whois, rc.WHOIS())
})
t.Run("existing_auto-client", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
ok := clients.addHost(ip, "host", client.SourceRDNS)
assert.True(t, ok)
clients.setWHOISInfo(ip, whois)
rc := clients.runtimeIndex.Client(ip)
require.NotNil(t, rc)
assert.Equal(t, whois, rc.WHOIS())
})
t.Run("can't_set_manually-added", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.2")
err := clients.storage.Add(&client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.2")},
})
require.NoError(t, err)
clients.setWHOISInfo(ip, whois)
rc := clients.runtimeIndex.Client(ip)
require.Nil(t, rc)
assert.True(t, clients.storage.RemoveByName("client1"))
})
}
func TestClientsAddExisting(t *testing.T) {
clients := newClientsContainer(t)
t.Run("simple", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
// Add a client.
err := clients.storage.Add(&client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{ip, netip.MustParseAddr("1:2:3::4")},
Subnets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
MACs: []net.HardwareAddr{{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}},
})
require.NoError(t, err)
// Now add an auto-client with the same IP.
ok := clients.addHost(ip, "test", client.SourceRDNS)
assert.True(t, ok)
})
t.Run("complicated", func(t *testing.T) {
// TODO(a.garipov): Properly decouple the DHCP server from the client
// storage.
if runtime.GOOS == "windows" {
t.Skip("skipping dhcp test on windows")
}
ip := netip.MustParseAddr("1.2.3.4")
// First, init a DHCP server with a single static lease.
config := &dhcpd.ServerConfig{
Enabled: true,
DataDir: t.TempDir(),
Conf4: dhcpd.V4ServerConf{
Enabled: true,
GatewayIP: netip.MustParseAddr("1.2.3.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("1.2.3.2"),
RangeEnd: netip.MustParseAddr("1.2.3.10"),
},
}
dhcpServer, err := dhcpd.Create(config)
require.NoError(t, err)
clients.dhcp = dhcpServer
err = dhcpServer.AddStaticLease(&dhcpsvc.Lease{
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: ip,
Hostname: "testhost",
Expiry: time.Now().Add(time.Hour),
})
require.NoError(t, err)
// Add a new client with the same IP as for a client with MAC.
err = clients.storage.Add(&client.Persistent{
Name: "client2",
UID: client.MustNewUID(),
IPs: []netip.Addr{ip},
})
require.NoError(t, err)
// Add a new client with the IP from the first client's IP range.
err = clients.storage.Add(&client.Persistent{
Name: "client3",
UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("2.2.2.2")},
})
require.NoError(t, err)
})
}
func TestClientsCustomUpstream(t *testing.T) {
clients := newClientsContainer(t)

View File

@@ -103,7 +103,9 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
return true
})
clients.runtimeIndex.Range(func(rc *client.Runtime) (cont bool) {
clients.storage.UpdateDHCP()
clients.storage.RangeRuntime(func(rc *client.Runtime) (cont bool) {
src, host := rc.Info()
cj := runtimeClientJSON{
WHOIS: whoisOrEmpty(rc),
@@ -117,18 +119,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
return true
})
for _, l := range clients.dhcp.Leases() {
cj := runtimeClientJSON{
Name: l.Hostname,
Source: client.SourceDHCP,
IP: l.IP,
WHOIS: &whois.Info{},
}
data.RuntimeClients = append(data.RuntimeClients, cj)
}
data.Tags = clientTags
data.Tags = clients.storage.AllowedTags()
aghhttp.WriteJSONResponseOK(w, r, data)
}
@@ -248,6 +239,7 @@ func copySafeSearch(
if conf.Enabled {
conf.Bing = true
conf.DuckDuckGo = true
conf.Ecosia = true
conf.Google = true
conf.Pixabay = true
conf.Yandex = true
@@ -429,7 +421,7 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
}
ip, _ := netip.ParseAddr(idStr)
c, ok := clients.find(idStr)
c, ok := clients.storage.Find(idStr)
var cj *clientJSON
if !ok {
cj = clients.findRuntime(ip, idStr)
@@ -451,7 +443,7 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
// non-nil.
func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *clientJSON) {
rc := clients.findRuntimeClient(ip)
rc := clients.storage.ClientRuntime(ip)
if rc == nil {
// It is still possible that the IP used to be in the runtime clients
// list, but then the server was reloaded. So, check the DNS server's

View File

@@ -1,27 +0,0 @@
package home
var clientTags = []string{
"device_audio",
"device_camera",
"device_gameconsole",
"device_laptop",
"device_nas", // Network-attached Storage
"device_other",
"device_pc",
"device_phone",
"device_printer",
"device_securityalarm",
"device_tablet",
"device_tv",
"os_android",
"os_ios",
"os_linux",
"os_macos",
"os_other",
"os_windows",
"user_admin",
"user_child",
"user_regular",
}

View File

@@ -423,6 +423,7 @@ var config = &configuration{
Enabled: false,
Bing: true,
DuckDuckGo: true,
Ecosia: true,
Google: true,
Pixabay: true,
Yandex: true,

View File

@@ -433,7 +433,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
// moment we'll allow setting up TLS in the initial configuration or the
// configuration itself will use HTTPS protocol, because the underlying
// functions potentially restart the HTTPS server.
err = startMods()
err = startMods(web.logger)
if err != nil {
Context.firstRun = true
copyInstallSettings(config, curConfig)

View File

@@ -1,7 +1,9 @@
package home
import (
"context"
"fmt"
"log/slog"
"net"
"net/netip"
"net/url"
@@ -19,6 +21,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/ameshkov/dnscrypt/v2"
yaml "gopkg.in/yaml.v3"
@@ -43,8 +46,8 @@ func onConfigModified() {
// initDNS updates all the fields of the [Context] needed to initialize the DNS
// server and initializes it at last. It also must not be called unless
// [config] and [Context] are initialized.
func initDNS() (err error) {
// [config] and [Context] are initialized. l must not be nil.
func initDNS(l *slog.Logger) (err error) {
anonymizer := config.anonymizer()
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
@@ -53,6 +56,7 @@ func initDNS() (err error) {
}
statsConf := stats.Config{
Logger: l.With(slogutil.KeyPrefix, "stats"),
Filename: filepath.Join(statsDir, "stats.db"),
Limit: config.Stats.Interval.Duration,
ConfigModified: onConfigModified,
@@ -113,13 +117,16 @@ func initDNS() (err error) {
anonymizer,
httpRegister,
tlsConf,
l,
)
}
// initDNSServer initializes the [context.dnsServer]. To only use the internal
// proxy, none of the arguments are required, but tlsConf 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.
// 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.
//
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
func initDNSServer(
filters *filtering.DNSFilter,
sts stats.Interface,
@@ -128,8 +135,10 @@ func initDNSServer(
anonymizer *aghnet.IPMut,
httpReg aghhttp.RegisterFunc,
tlsConf *tlsConfigSettings,
l *slog.Logger,
) (err error) {
Context.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
Logger: l,
DNSFilter: filters,
Stats: sts,
QueryLog: qlog,
@@ -406,9 +415,9 @@ func applyAdditionalFiltering(clientIP netip.Addr, clientID string, setts *filte
setts.ClientIP = clientIP
c, ok := Context.clients.find(clientID)
c, ok := Context.clients.storage.Find(clientID)
if !ok {
c, ok = Context.clients.find(clientIP.String())
c, ok = Context.clients.storage.Find(clientIP.String())
if !ok {
log.Debug("%s: no clients with ip %s and clientid %q", pref, clientIP, clientID)
@@ -451,11 +460,15 @@ func startDNSServer() error {
Context.filters.EnableFilters(false)
Context.clients.Start()
err := Context.dnsServer.Start()
// TODO(s.chzhen): Pass context.
err := Context.clients.Start(context.TODO())
if err != nil {
return fmt.Errorf("couldn't start forwarding DNS server: %w", err)
return fmt.Errorf("starting clients container: %w", err)
}
err = Context.dnsServer.Start()
if err != nil {
return fmt.Errorf("starting dns server: %w", err)
}
Context.filters.Start()
@@ -492,7 +505,7 @@ func stopDNSServer() (err error) {
return fmt.Errorf("stopping forwarding dns server: %w", err)
}
err = Context.clients.close()
err = Context.clients.close(context.TODO())
if err != nil {
return fmt.Errorf("closing clients container: %w", err)
}

View File

@@ -18,9 +18,8 @@ var testIPv4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})
func newStorage(tb testing.TB, clients []*client.Persistent) (s *client.Storage) {
tb.Helper()
s = client.NewStorage(&client.Config{
AllowedTags: nil,
})
s, err := client.NewStorage(&client.StorageConfig{})
require.NoError(tb, err)
for _, p := range clients {
p.UID = client.MustNewUID()

View File

@@ -6,6 +6,7 @@ import (
"crypto/x509"
"fmt"
"io/fs"
"log/slog"
"net/http"
"net/netip"
"net/url"
@@ -38,6 +39,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil"
)
@@ -90,6 +92,8 @@ func (c *homeContext) getDataDir() string {
}
// Context - a global context object
//
// TODO(a.garipov): Refactor.
var Context homeContext
// Main is the entry point
@@ -115,7 +119,7 @@ func Main(clientBuildFS fs.FS) {
log.Info("Received signal %q", sig)
switch sig {
case syscall.SIGHUP:
Context.clients.reloadARP()
Context.clients.storage.ReloadARP()
Context.tls.reload()
default:
cleanup(context.Background())
@@ -273,7 +277,7 @@ func setupOpts(opts options) (err error) {
}
// initContextClients initializes Context clients and related fields.
func initContextClients() (err error) {
func initContextClients(logger *slog.Logger) (err error) {
err = setupDNSFilteringConf(config.Filtering)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
@@ -297,7 +301,7 @@ func initContextClients() (err error) {
var arpDB arpdb.Interface
if config.Clients.Sources.ARP {
arpDB = arpdb.New()
arpDB = arpdb.New(logger.With(slogutil.KeyError, "arpdb"))
}
return Context.clients.Init(
@@ -482,7 +486,12 @@ func checkPorts() (err error) {
return nil
}
func initWeb(opts options, clientBuildFS fs.FS, upd *updater.Updater) (web *webAPI, err error) {
func initWeb(
opts options,
clientBuildFS fs.FS,
upd *updater.Updater,
l *slog.Logger,
) (web *webAPI, err error) {
var clientFS fs.FS
if opts.localFrontend {
log.Info("warning: using local frontend files")
@@ -524,7 +533,7 @@ func initWeb(opts options, clientBuildFS fs.FS, upd *updater.Updater) (web *webA
serveHTTP3: config.DNS.ServeHTTP3,
}
web = newWebAPI(webConf)
web = newWebAPI(webConf, l)
if web == nil {
return nil, fmt.Errorf("initializing web: %w", err)
}
@@ -547,10 +556,15 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
// Configure config filename.
initConfigFilename(opts)
ls := getLogSettings(opts)
// Configure log level and output.
err = configureLogger(opts)
err = configureLogger(ls)
fatalOnError(err)
// TODO(a.garipov): Use slog everywhere.
slogLogger := newSlogLogger(ls)
// Print the first message after logger is configured.
log.Info(version.Full())
log.Debug("current working directory is %s", Context.workDir)
@@ -569,7 +583,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
// data first, but also to avoid relying on automatic Go init() function.
filtering.InitModule()
err = initContextClients()
err = initContextClients(slogLogger)
fatalOnError(err)
err = setupOpts(opts)
@@ -604,7 +618,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(opts, upd)
cmdlineUpdate(opts, upd, slogLogger)
if !Context.firstRun {
// Save the updated config.
@@ -632,11 +646,11 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
onConfigModified()
}
Context.web, err = initWeb(opts, clientBuildFS, upd)
Context.web, err = initWeb(opts, clientBuildFS, upd, slogLogger)
fatalOnError(err)
if !Context.firstRun {
err = initDNS()
err = initDNS(slogLogger)
fatalOnError(err)
Context.tls.start()
@@ -697,9 +711,10 @@ func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
return aghnet.NewIPMut(anonFunc)
}
// startMods initializes and starts the DNS server after installation.
func startMods() (err error) {
err = initDNS()
// startMods initializes and starts the DNS server after installation. l must
// not be nil.
func startMods(l *slog.Logger) (err error) {
err = initDNS(l)
if err != nil {
return err
}
@@ -959,8 +974,8 @@ type jsonError struct {
Message string `json:"message"`
}
// cmdlineUpdate updates current application and exits.
func cmdlineUpdate(opts options, upd *updater.Updater) {
// cmdlineUpdate updates current application and exits. l must not be nil.
func cmdlineUpdate(opts options, upd *updater.Updater, l *slog.Logger) {
if !opts.performUpdate {
return
}
@@ -970,7 +985,7 @@ func cmdlineUpdate(opts options, upd *updater.Updater) {
//
// TODO(e.burkov): We could probably initialize the internal resolver
// separately.
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, l)
fatalOnError(err)
log.Info("cmdline update: performing update")

View File

@@ -3,11 +3,13 @@ package home
import (
"cmp"
"fmt"
"log/slog"
"path/filepath"
"runtime"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/yaml.v3"
)
@@ -16,10 +18,21 @@ import (
// for logger output.
const configSyslog = "syslog"
// configureLogger configures logger level and output.
func configureLogger(opts options) (err error) {
ls := getLogSettings(opts)
// newSlogLogger returns new [*slog.Logger] configured with the given settings.
func newSlogLogger(ls *logSettings) (l *slog.Logger) {
if !ls.Enabled {
return slogutil.NewDiscardLogger()
}
return slogutil.New(&slogutil.Config{
Format: slogutil.FormatAdGuardLegacy,
AddTimestamp: true,
Verbose: ls.Verbose,
})
}
// configureLogger configures logger level and output.
func configureLogger(ls *logSettings) (err error) {
// Configure logger level.
if !ls.Enabled {
log.SetLevel(log.OFF)
@@ -60,7 +73,7 @@ func configureLogger(opts options) (err error) {
MaxAge: ls.MaxAge,
})
return nil
return err
}
// getLogSettings returns a log settings object properly initialized from opts.

View File

@@ -5,12 +5,15 @@ import (
"net/http"
"github.com/AdguardTeam/golibs/ioutil"
"github.com/c2h5oh/datasize"
)
// middlerware is a wrapper function signature.
type middleware func(http.Handler) http.Handler
// withMiddlewares consequently wraps h with all the middlewares.
//
// TODO(e.burkov): Use [httputil.Wrap].
func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Handler) {
wrapped = h
@@ -23,11 +26,11 @@ func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Ha
const (
// defaultReqBodySzLim is the default maximum request body size.
defaultReqBodySzLim = 64 * 1024
defaultReqBodySzLim datasize.ByteSize = 64 * datasize.KB
// largerReqBodySzLim is the maximum request body size for APIs expecting
// larger requests.
largerReqBodySzLim = 4 * 1024 * 1024
largerReqBodySzLim datasize.ByteSize = 4 * datasize.MB
)
// expectsLargerRequests shows if this request should use a larger body size
@@ -38,26 +41,28 @@ const (
// See https://github.com/AdguardTeam/AdGuardHome/issues/2666 and
// https://github.com/AdguardTeam/AdGuardHome/issues/2675.
func expectsLargerRequests(r *http.Request) (ok bool) {
m := r.Method
if m != http.MethodPost {
if r.Method != http.MethodPost {
return false
}
p := r.URL.Path
return p == "/control/access/set" ||
p == "/control/filtering/set_rules"
switch r.URL.Path {
case "/control/access/set", "/control/filtering/set_rules":
return true
default:
return false
}
}
// limitRequestBody wraps underlying handler h, making it's request's body Read
// method limited.
func limitRequestBody(h http.Handler) (limited http.Handler) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var szLim uint64 = defaultReqBodySzLim
szLim := defaultReqBodySzLim
if expectsLargerRequests(r) {
szLim = largerReqBodySzLim
}
reader := ioutil.LimitReader(r.Body, szLim)
reader := ioutil.LimitReader(r.Body, szLim.Bytes())
// HTTP handlers aren't supposed to call r.Body.Close(), so just
// replace the body in a clone.

View File

@@ -14,29 +14,29 @@ import (
func TestLimitRequestBody(t *testing.T) {
errReqLimitReached := &ioutil.LimitError{
Limit: defaultReqBodySzLim,
Limit: defaultReqBodySzLim.Bytes(),
}
testCases := []struct {
wantErr error
name string
body string
want []byte
wantErr error
}{{
wantErr: nil,
name: "not_so_big",
body: "somestr",
want: []byte("somestr"),
wantErr: nil,
}, {
wantErr: errReqLimitReached,
name: "so_big",
body: string(make([]byte, defaultReqBodySzLim+1)),
want: make([]byte, defaultReqBodySzLim),
wantErr: errReqLimitReached,
}, {
wantErr: nil,
name: "empty",
body: "",
want: []byte(nil),
wantErr: nil,
}}
makeHandler := func(t *testing.T, err *error) http.HandlerFunc {

View File

@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"io/fs"
"log/slog"
"net/http"
"net/netip"
"runtime"
@@ -16,7 +17,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/pprofutil"
"github.com/AdguardTeam/golibs/netutil/httputil"
"github.com/NYTimes/gziphandler"
"github.com/quic-go/quic-go/http3"
"golang.org/x/net/http2"
@@ -90,17 +91,22 @@ type webAPI struct {
// TODO(a.garipov): Refactor all these servers.
httpServer *http.Server
// logger is a slog logger used in webAPI. It must not be nil.
logger *slog.Logger
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
// [Web.http3Server] must also not be nil.
httpsServer httpsServer
}
// newWebAPI creates a new instance of the web UI and API server.
func newWebAPI(conf *webConfig) (w *webAPI) {
// newWebAPI creates a new instance of the web UI and API server. l must not be
// nil.
func newWebAPI(conf *webConfig, l *slog.Logger) (w *webAPI) {
log.Info("web: initializing")
w = &webAPI{
conf: conf,
conf: conf,
logger: l,
}
clientFS := http.FileServer(http.FS(conf.clientFS))
@@ -327,7 +333,7 @@ func startPprof(port uint16) {
runtime.SetMutexProfileFraction(1)
mux := http.NewServeMux()
pprofutil.RoutePprof(mux)
httputil.RoutePprof(mux)
go func() {
defer log.OnPanic("pprof server")

View File

@@ -2,6 +2,8 @@
package ipset
import (
"context"
"log/slog"
"net"
)
@@ -10,24 +12,33 @@ import (
// TODO(a.garipov): Perhaps generalize this into some kind of a NetFilter type,
// since ipset is exclusive to Linux?
type Manager interface {
Add(host string, ip4s, ip6s []net.IP) (n int, err error)
Add(ctx context.Context, host string, ip4s, ip6s []net.IP) (n int, err error)
Close() (err error)
}
// NewManager returns a new ipset manager. IPv4 addresses are added to an
// ipset with an ipv4 family; IPv6 addresses, to an ipv6 ipset. ipset must
// exist.
// Config is the configuration structure for the ipset manager.
type Config struct {
// Logger is used for logging the operation of the ipset manager. It must
// not be nil.
Logger *slog.Logger
// Lines is the ipset configuration with the following syntax:
//
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
//
// Lines must not contain any blank lines or comments.
Lines []string
}
// NewManager returns a new ipset manager. IPv4 addresses are added to an ipset
// with an ipv4 family; IPv6 addresses, to an ipv6 ipset. ipset must exist.
//
// The syntax of the ipsetConf is:
//
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
//
// If ipsetConf is empty, msg and err are nil. The error's chain contains
// If conf.Lines is empty, mgr and err are nil. The error's chain contains
// [errors.ErrUnsupported] if current OS is not supported.
func NewManager(ipsetConf []string) (mgr Manager, err error) {
if len(ipsetConf) == 0 {
func NewManager(ctx context.Context, conf *Config) (mgr Manager, err error) {
if len(conf.Lines) == 0 {
return nil, nil
}
return newManager(ipsetConf)
return newManager(ctx, conf)
}

View File

@@ -4,14 +4,16 @@ package ipset
import (
"bytes"
"context"
"fmt"
"log/slog"
"net"
"strings"
"sync"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/digineo/go-ipset/v2"
"github.com/mdlayher/netlink"
"github.com/ti-mo/netfilter"
@@ -34,8 +36,8 @@ import (
// resolved IP addresses.
// newManager returns a new Linux ipset manager.
func newManager(ipsetConf []string) (set Manager, err error) {
return newManagerWithDialer(ipsetConf, defaultDial)
func newManager(ctx context.Context, conf *Config) (set Manager, err error) {
return newManagerWithDialer(ctx, conf, defaultDial)
}
// defaultDial is the default netfilter dialing function.
@@ -180,6 +182,8 @@ type manager struct {
nameToIpset map[string]props
domainToIpsets map[string][]props
logger *slog.Logger
dial dialer
// mu protects all properties below.
@@ -254,7 +258,7 @@ func parseIpsetConfigLine(confStr string) (hosts, ipsetNames []string, err error
// parseIpsetConfig parses the ipset configuration and stores ipsets. It
// returns an error if the configuration can't be used.
func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) {
func (m *manager) parseIpsetConfig(ctx context.Context, ipsetConf []string) (err error) {
// The family doesn't seem to matter when we use a header query, so query
// only the IPv4 one.
//
@@ -278,7 +282,7 @@ func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) {
}
var ipsets []props
ipsets, err = m.ipsets(ipsetNames, currentlyKnown)
ipsets, err = m.ipsets(ctx, ipsetNames, currentlyKnown)
if err != nil {
return fmt.Errorf("getting ipsets from config line at idx %d: %w", i, err)
}
@@ -328,7 +332,11 @@ func (m *manager) ipsetProps(name string) (p props, err error) {
// ipsets returns ipset properties of currently known ipsets. It also makes an
// additional ipset header data query if needed.
func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets []props, err error) {
func (m *manager) ipsets(
ctx context.Context,
names []string,
currentlyKnown map[string]props,
) (sets []props, err error) {
for _, n := range names {
p, ok := currentlyKnown[n]
if !ok {
@@ -336,10 +344,12 @@ func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets
}
if p.family != netfilter.ProtoIPv4 && p.family != netfilter.ProtoIPv6 {
log.Debug("ipset: getting properties: %q %q unexpected ipset family %q",
p.name,
p.typeName,
p.family,
m.logger.DebugContext(
ctx,
"got unexpected ipset family while getting set properties",
"set_name", p.name,
"set_type", p.typeName,
"set_family", p.family,
)
p, err = m.ipsetProps(n)
@@ -357,7 +367,7 @@ func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets
// newManagerWithDialer returns a new Linux ipset manager using the provided
// dialer.
func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err error) {
func newManagerWithDialer(ctx context.Context, conf *Config, dial dialer) (mgr Manager, err error) {
defer func() { err = errors.Annotate(err, "ipset: %w") }()
m := &manager{
@@ -366,6 +376,8 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
nameToIpset: make(map[string]props),
domainToIpsets: make(map[string][]props),
logger: conf.Logger,
dial: dial,
addedIPs: container.NewMapSet[ipInIpsetEntry](),
@@ -376,7 +388,7 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
if errors.Is(err, unix.EPROTONOSUPPORT) {
// The implementation doesn't support this protocol version. Just
// issue a warning.
log.Info("ipset: dialing netfilter: warning: %s", err)
m.logger.WarnContext(ctx, "dialing netfilter", slogutil.KeyError, err)
return nil, nil
}
@@ -384,12 +396,12 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
return nil, fmt.Errorf("dialing netfilter: %w", err)
}
err = m.parseIpsetConfig(ipsetConf)
err = m.parseIpsetConfig(ctx, conf.Lines)
if err != nil {
return nil, fmt.Errorf("getting ipsets: %w", err)
}
log.Debug("ipset: initialized")
m.logger.DebugContext(ctx, "initialized")
return m, nil
}
@@ -476,6 +488,7 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
// addToSets adds the IP addresses to the corresponding ipset.
func (m *manager) addToSets(
ctx context.Context,
host string,
ip4s []net.IP,
ip6s []net.IP,
@@ -498,7 +511,13 @@ func (m *manager) addToSets(
return n, fmt.Errorf("%q %q unexpected family %q", set.name, set.typeName, set.family)
}
log.Debug("ipset: added %d ips to set %q %q", nn, set.name, set.typeName)
m.logger.DebugContext(
ctx,
"added ips to set",
"ips_num", nn,
"set_name", set.name,
"set_type", set.typeName,
)
n += nn
}
@@ -507,7 +526,7 @@ func (m *manager) addToSets(
}
// Add implements the [Manager] interface for *manager.
func (m *manager) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
func (m *manager) Add(ctx context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {
m.mu.Lock()
defer m.mu.Unlock()
@@ -516,9 +535,9 @@ func (m *manager) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
return 0, nil
}
log.Debug("ipset: found %d sets", len(sets))
m.logger.DebugContext(ctx, "found sets", "set_num", len(sets))
return m.addToSets(host, ip4s, ip6s, sets)
return m.addToSets(ctx, host, ip4s, ip6s, sets)
}
// Close implements the [Manager] interface for *manager.

View File

@@ -6,8 +6,11 @@ import (
"net"
"strings"
"testing"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/digineo/go-ipset/v2"
"github.com/mdlayher/netlink"
"github.com/stretchr/testify/assert"
@@ -15,6 +18,9 @@ import (
"github.com/ti-mo/netfilter"
)
// testTimeout is a common timeout for tests and contexts.
const testTimeout = 1 * time.Second
// fakeConn is a fake ipsetConn for tests.
type fakeConn struct {
ipv4Header *ipset.HeaderPolicy
@@ -58,7 +64,7 @@ func (c *fakeConn) listAll() (sets []props, err error) {
}
func TestManager_Add(t *testing.T) {
ipsetConf := []string{
ipsetList := []string{
"example.com,example.net/ipv4set",
"example.org,example.biz/ipv6set",
}
@@ -89,7 +95,11 @@ func TestManager_Add(t *testing.T) {
}, nil
}
m, err := newManagerWithDialer(ipsetConf, fakeDial)
conf := &Config{
Logger: slogutil.NewDiscardLogger(),
Lines: ipsetList,
}
m, err := newManagerWithDialer(testutil.ContextWithTimeout(t, testTimeout), conf, fakeDial)
require.NoError(t, err)
ip4 := net.IP{1, 2, 3, 4}
@@ -100,7 +110,7 @@ func TestManager_Add(t *testing.T) {
0x00, 0x00, 0x56, 0x78,
}
n, err := m.Add("example.net", []net.IP{ip4}, nil)
n, err := m.Add(testutil.ContextWithTimeout(t, testTimeout), "example.net", []net.IP{ip4}, nil)
require.NoError(t, err)
assert.Equal(t, 1, n)
@@ -110,7 +120,7 @@ func TestManager_Add(t *testing.T) {
gotIP4 := ipv4Entries[0].IP.Value
assert.Equal(t, ip4, gotIP4)
n, err = m.Add("example.biz", nil, []net.IP{ip6})
n, err = m.Add(testutil.ContextWithTimeout(t, testTimeout), "example.biz", nil, []net.IP{ip6})
require.NoError(t, err)
assert.Equal(t, 1, n)

View File

@@ -3,9 +3,11 @@
package ipset
import (
"context"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
)
func newManager(_ []string) (mgr Manager, err error) {
func newManager(_ context.Context, _ *Config) (mgr Manager, err error) {
return nil, aghos.Unsupported("ipset")
}

View File

@@ -24,7 +24,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/pprofutil"
"github.com/AdguardTeam/golibs/netutil/httputil"
httptreemux "github.com/dimfeld/httptreemux/v5"
)
@@ -107,7 +107,7 @@ func (svc *Service) setupPprof(c *PprofConfig) {
runtime.SetMutexProfileFraction(1)
pprofMux := http.NewServeMux()
pprofutil.RoutePprof(pprofMux)
httputil.RoutePprof(pprofMux)
svc.pprofPort = c.Port
addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)

View File

@@ -7,9 +7,9 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil"
@@ -32,7 +32,7 @@ type queryLog struct {
// buffer contains recent log entries. The entries in this buffer must not
// be modified.
buffer *aghalg.RingBuffer[*logEntry]
buffer *container.RingBuffer[*logEntry]
// logFile is the path to the log file.
logFile string
@@ -225,7 +225,7 @@ func (l *queryLog) Add(params *AddParams) {
l.bufferLock.Lock()
defer l.bufferLock.Unlock()
l.buffer.Append(entry)
l.buffer.Push(entry)
if !l.flushPending && fileIsEnabled && l.buffer.Len() >= memSize {
l.flushPending = true

View File

@@ -7,10 +7,10 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/miekg/dns"
)
@@ -153,7 +153,7 @@ func newQueryLog(conf Config) (l *queryLog, err error) {
l = &queryLog{
findClient: findClient,
buffer: aghalg.NewRingBuffer[*logEntry](memSize),
buffer: container.NewRingBuffer[*logEntry](memSize),
conf: &Config{},
confMu: &sync.RWMutex{},

View File

@@ -2,11 +2,13 @@
package rdns
import (
"context"
"log/slog"
"net/netip"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/bluele/gcache"
)
@@ -14,7 +16,7 @@ import (
type Interface interface {
// Process makes rDNS request and returns domain name. changed indicates
// that domain name was updated since last request.
Process(ip netip.Addr) (host string, changed bool)
Process(ctx context.Context, ip netip.Addr) (host string, changed bool)
}
// Empty is an empty [Interface] implementation which does nothing.
@@ -24,7 +26,7 @@ type Empty struct{}
var _ Interface = (*Empty)(nil)
// Process implements the [Interface] interface for Empty.
func (Empty) Process(_ netip.Addr) (host string, changed bool) {
func (Empty) Process(_ context.Context, _ netip.Addr) (host string, changed bool) {
return "", false
}
@@ -37,6 +39,10 @@ type Exchanger interface {
// Config is the configuration structure for Default.
type Config struct {
// Logger is used for logging the operation of the reverse DNS lookup
// queries. It must not be nil.
Logger *slog.Logger
// Exchanger resolves IP addresses to domain names.
Exchanger Exchanger
@@ -50,6 +56,10 @@ type Config struct {
// Default is the default rDNS query processor.
type Default struct {
// logger is used for logging the operation of the reverse DNS lookup
// queries. It must not be nil.
logger *slog.Logger
// cache is the cache containing IP addresses of clients. An active IP
// address is resolved once again after it expires. If IP address couldn't
// be resolved, it stays here for some time to prevent further attempts to
@@ -66,6 +76,7 @@ type Default struct {
// New returns a new default rDNS query processor. conf must not be nil.
func New(conf *Config) (r *Default) {
return &Default{
logger: conf.Logger,
cache: gcache.New(conf.CacheSize).LRU().Build(),
exchanger: conf.Exchanger,
cacheTTL: conf.CacheTTL,
@@ -76,15 +87,15 @@ func New(conf *Config) (r *Default) {
var _ Interface = (*Default)(nil)
// Process implements the [Interface] interface for Default.
func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
fromCache, expired := r.findInCache(ip)
func (r *Default) Process(ctx context.Context, ip netip.Addr) (host string, changed bool) {
fromCache, expired := r.findInCache(ctx, ip)
if !expired {
return fromCache, false
}
host, ttl, err := r.exchanger.Exchange(ip)
if err != nil {
log.Debug("rdns: resolving %q: %s", ip, err)
r.logger.DebugContext(ctx, "resolving", "ip", ip, slogutil.KeyError, err)
}
ttl = max(ttl, r.cacheTTL)
@@ -96,7 +107,7 @@ func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
err = r.cache.Set(ip, item)
if err != nil {
log.Debug("rdns: cache: adding item %q: %s", ip, err)
r.logger.DebugContext(ctx, "adding item to cache", "key", ip, slogutil.KeyError, err)
}
// TODO(e.burkov): The name doesn't change if it's neither stored in cache
@@ -106,22 +117,22 @@ func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
// findInCache finds domain name in the cache. expired is true if host is not
// valid anymore.
func (r *Default) findInCache(ip netip.Addr) (host string, expired bool) {
func (r *Default) findInCache(ctx context.Context, ip netip.Addr) (host string, expired bool) {
val, err := r.cache.Get(ip)
if err != nil {
if !errors.Is(err, gcache.KeyNotFoundError) {
log.Debug("rdns: cache: retrieving %q: %s", ip, err)
r.logger.DebugContext(
ctx,
"retrieving item from cache",
"key", ip,
slogutil.KeyError, err,
)
}
return "", true
}
item, ok := val.(*cacheItem)
if !ok {
log.Debug("rdns: cache: %q bad type %T", ip, val)
return "", true
}
item := val.(*cacheItem)
return item.host, time.Now().After(item.expiry)
}

View File

@@ -8,10 +8,14 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTimeout is a common timeout for tests and contexts.
const testTimeout = 1 * time.Second
func TestDefault_Process(t *testing.T) {
ip1 := netip.MustParseAddr("1.2.3.4")
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
@@ -71,14 +75,14 @@ func TestDefault_Process(t *testing.T) {
Exchanger: &aghtest.Exchanger{OnExchange: onExchange},
})
got, changed := r.Process(tc.addr)
got, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)
require.True(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
// From cache.
got, changed = r.Process(tc.addr)
got, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)
require.False(t, changed)
assert.Equal(t, tc.want, got)
@@ -101,7 +105,7 @@ func TestDefault_Process(t *testing.T) {
Exchanger: zeroTTLExchanger,
})
got, changed := r.Process(ip1)
got, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)
require.True(t, changed)
assert.Equal(t, revAddr1, got)
@@ -109,14 +113,15 @@ func TestDefault_Process(t *testing.T) {
return revAddr2, time.Hour, nil
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
require.EventuallyWithT(t, func(t *assert.CollectT) {
got, changed = r.Process(ip1)
got, changed = r.Process(ctx, ip1)
assert.True(t, changed)
assert.Equal(t, revAddr2, got)
}, 2*cacheTTL, time.Millisecond*100)
assert.Never(t, func() (changed bool) {
_, changed = r.Process(ip1)
_, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)
return changed
}, 2*cacheTTL, time.Millisecond*100)

View File

@@ -10,7 +10,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil"
)
@@ -51,6 +50,8 @@ type StatsResp struct {
func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ctx := r.Context()
var (
resp *StatsResp
ok bool
@@ -62,12 +63,17 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
resp, ok = s.getData(uint32(s.limit.Hours()))
}()
log.Debug("stats: prepared data in %v", time.Since(start))
s.logger.DebugContext(
ctx,
"prepared data",
"elapsed", timeutil.Duration{Duration: time.Since(start)},
)
if !ok {
// Don't bring the message to the lower case since it's a part of UI
// text for the moment.
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't get statistics data")
const msg = "Couldn't get statistics data"
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusInternalServerError, msg)
return
}
@@ -146,16 +152,18 @@ func (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request)
//
// Deprecated: Remove it when migration to the new API is over.
func (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqData := configResp{}
err := json.NewDecoder(r.Body).Decode(&reqData)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
if !checkInterval(reqData.IntervalDays) {
aghhttp.Error(r, w, http.StatusBadRequest, "Unsupported interval")
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, "Unsupported interval")
return
}
@@ -173,17 +181,19 @@ func (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
// handlePutStatsConfig is the handler for the PUT /control/stats/config/update
// HTTP API.
func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqData := getConfigResp{}
err := json.NewDecoder(r.Body).Decode(&reqData)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
engine, err := aghnet.NewIgnoreEngine(reqData.Ignored)
if err != nil {
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "ignored: %s", err)
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusUnprocessableEntity, "ignored: %s", err)
return
}
@@ -191,13 +201,21 @@ func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request)
ivl := time.Duration(reqData.Interval) * time.Millisecond
err = validateIvl(ivl)
if err != nil {
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "unsupported interval: %s", err)
aghhttp.ErrorAndLog(
ctx,
s.logger,
r,
w,
http.StatusUnprocessableEntity,
"unsupported interval: %s",
err,
)
return
}
if reqData.Enabled == aghalg.NBNull {
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "enabled is null")
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusUnprocessableEntity, "enabled is null")
return
}
@@ -216,7 +234,15 @@ func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request)
func (s *StatsCtx) handleStatsReset(w http.ResponseWriter, r *http.Request) {
err := s.clear()
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "stats: %s", err)
aghhttp.ErrorAndLog(
r.Context(),
s.logger,
r,
w,
http.StatusInternalServerError,
"stats: %s",
err,
)
}
}

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
@@ -24,6 +25,7 @@ func TestHandleStatsConfig(t *testing.T) {
)
conf := Config{
Logger: slogutil.NewDiscardLogger(),
UnitID: func() (id uint32) { return 0 },
ConfigModified: func() {},
ShouldCountClient: func([]string) bool { return true },

View File

@@ -3,8 +3,10 @@
package stats
import (
"context"
"fmt"
"io"
"log/slog"
"net/netip"
"os"
"sync"
@@ -14,7 +16,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"go.etcd.io/bbolt"
)
@@ -43,6 +45,10 @@ func validateIvl(ivl time.Duration) (err error) {
//
// Do not alter any fields of this structure after using it.
type Config struct {
// Logger is used for logging the operation of the statistics management.
// It must not be nil.
Logger *slog.Logger
// UnitID is the function to generate the identifier for current unit. If
// nil, the default function is used, see newUnitID.
UnitID UnitIDGenFunc
@@ -96,6 +102,10 @@ type Interface interface {
// StatsCtx collects the statistics and flushes it to the database. Its default
// flushing interval is one hour.
type StatsCtx struct {
// logger is used for logging the operation of the statistics management.
// It must not be nil.
logger *slog.Logger
// currMu protects curr.
currMu *sync.RWMutex
// curr is the actual statistics collection result.
@@ -150,6 +160,7 @@ func New(conf Config) (s *StatsCtx, err error) {
}
s = &StatsCtx{
logger: conf.Logger,
currMu: &sync.RWMutex{},
httpRegister: conf.HTTPRegister,
configModified: conf.ConfigModified,
@@ -178,21 +189,21 @@ func New(conf Config) (s *StatsCtx, err error) {
tx, err := s.db.Load().Begin(true)
if err != nil {
return nil, fmt.Errorf("stats: opening a transaction: %w", err)
return nil, fmt.Errorf("opening a transaction: %w", err)
}
deleted := deleteOldUnits(tx, id-uint32(s.limit.Hours())-1)
udb = loadUnitFromDB(tx, id)
deleted := s.deleteOldUnits(tx, id-uint32(s.limit.Hours())-1)
udb = s.loadUnitFromDB(tx, id)
err = finishTxn(tx, deleted > 0)
if err != nil {
log.Error("stats: %s", err)
s.logger.Error("finishing transacation", slogutil.KeyError, err)
}
s.curr = newUnit(id)
s.curr.deserialize(udb)
log.Debug("stats: initialized")
s.logger.Debug("initialized")
return s, nil
}
@@ -228,8 +239,6 @@ func (s *StatsCtx) Start() {
// Close implements the [io.Closer] interface for *StatsCtx.
func (s *StatsCtx) Close() (err error) {
defer func() { err = errors.Annotate(err, "stats: closing: %w") }()
db := s.db.Swap(nil)
if db == nil {
return nil
@@ -237,7 +246,7 @@ func (s *StatsCtx) Close() (err error) {
defer func() {
cerr := db.Close()
if cerr == nil {
log.Debug("stats: database closed")
s.logger.Debug("database closed")
}
err = errors.WithDeferred(err, cerr)
@@ -254,7 +263,7 @@ func (s *StatsCtx) Close() (err error) {
udb := s.curr.serialize()
return udb.flushUnitToDB(tx, s.curr.id)
return s.flushUnitToDB(udb, tx, s.curr.id)
}
// Update implements the [Interface] interface for *StatsCtx. e must not be
@@ -269,7 +278,7 @@ func (s *StatsCtx) Update(e *Entry) {
err := e.validate()
if err != nil {
log.Debug("stats: updating: validating entry: %s", err)
s.logger.Debug("validating entry", slogutil.KeyError, err)
return
}
@@ -278,7 +287,7 @@ func (s *StatsCtx) Update(e *Entry) {
defer s.currMu.Unlock()
if s.curr == nil {
log.Error("stats: current unit is nil")
s.logger.Error("current unit is nil")
return
}
@@ -333,8 +342,8 @@ func (s *StatsCtx) TopClientsIP(maxCount uint) (ips []netip.Addr) {
// deleteOldUnits walks the buckets available to tx and deletes old units. It
// returns the number of deletions performed.
func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
log.Debug("stats: deleting old units until id %d", firstID)
func (s *StatsCtx) deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
s.logger.Debug("deleting old units up to", "unit", firstID)
// TODO(a.garipov): See if this is actually necessary. Looks like a rather
// bizarre solution.
@@ -348,12 +357,12 @@ func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
err = tx.DeleteBucket(name)
if err != nil {
log.Debug("stats: deleting bucket: %s", err)
s.logger.Debug("deleting bucket", slogutil.KeyError, err)
return nil
}
log.Debug("stats: deleted unit %d (name %x)", nameID, name)
s.logger.Debug("deleted unit", "name_id", nameID, "name", fmt.Sprintf("%x", name))
deleted++
@@ -362,7 +371,7 @@ func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
err := tx.ForEach(walk)
if err != nil && !errors.Is(err, errStop) {
log.Debug("stats: deleting units: %s", err)
s.logger.Debug("deleting units", slogutil.KeyError, err)
}
return deleted
@@ -371,20 +380,29 @@ func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
// openDB returns an error if the database can't be opened from the specified
// file. It's safe for concurrent use.
func (s *StatsCtx) openDB() (err error) {
log.Debug("stats: opening database")
s.logger.Debug("opening database")
var db *bbolt.DB
db, err = bbolt.Open(s.filename, 0o644, nil)
if err != nil {
if err.Error() == "invalid argument" {
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations")
const lines = `AdGuard Home cannot be initialized due to an incompatible file system.
Please read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations`
// TODO(s.chzhen): Use passed context.
slogutil.PrintLines(
context.TODO(),
s.logger,
slog.LevelError,
"opening database",
lines,
)
}
return err
}
// Use defer to unlock the mutex as soon as possible.
defer log.Debug("stats: database opened")
defer s.logger.Debug("database opened")
s.db.Store(db)
@@ -424,34 +442,37 @@ func (s *StatsCtx) flushDB(id, limit uint32, ptr *unit) (cont bool, sleepFor tim
isCommitable := true
tx, err := db.Begin(true)
if err != nil {
log.Error("stats: opening transaction: %s", err)
s.logger.Error("opening transaction", slogutil.KeyError, err)
return true, 0
}
defer func() {
if err = finishTxn(tx, isCommitable); err != nil {
log.Error("stats: %s", err)
s.logger.Error("finishing transaction", slogutil.KeyError, err)
}
}()
s.curr = newUnit(id)
flushErr := ptr.serialize().flushUnitToDB(tx, ptr.id)
udb := ptr.serialize()
flushErr := s.flushUnitToDB(udb, tx, ptr.id)
if flushErr != nil {
log.Error("stats: flushing unit: %s", flushErr)
s.logger.Error("flushing unit", slogutil.KeyError, flushErr)
isCommitable = false
}
delErr := tx.DeleteBucket(idToUnitName(id - limit))
if delErr != nil {
// TODO(e.burkov): Improve the algorithm of deleting the oldest bucket
// to avoid the error.
if errors.Is(delErr, bbolt.ErrBucketNotFound) {
log.Debug("stats: warning: deleting unit: %s", delErr)
} else {
lvl := slog.LevelWarn
if !errors.Is(delErr, bbolt.ErrBucketNotFound) {
isCommitable = false
log.Error("stats: deleting unit: %s", delErr)
lvl = slog.LevelError
}
s.logger.Log(context.TODO(), lvl, "deleting bucket", slogutil.KeyError, delErr)
}
return true, 0
@@ -467,7 +488,7 @@ func (s *StatsCtx) periodicFlush() {
cont, sleepFor = s.flush()
}
log.Debug("periodic flushing finished")
s.logger.Debug("periodic flushing finished")
}
// setLimit sets the limit. s.lock is expected to be locked.
@@ -477,16 +498,16 @@ func (s *StatsCtx) setLimit(limit time.Duration) {
if limit != 0 {
s.enabled = true
s.limit = limit
log.Debug("stats: set limit: %d days", limit/timeutil.Day)
s.logger.Debug("setting limit in days", "num", limit/timeutil.Day)
return
}
s.enabled = false
log.Debug("stats: disabled")
s.logger.Debug("disabled")
if err := s.clear(); err != nil {
log.Error("stats: %s", err)
s.logger.Error("clearing", slogutil.KeyError, err)
}
}
@@ -499,7 +520,7 @@ func (s *StatsCtx) clear() (err error) {
var tx *bbolt.Tx
tx, err = db.Begin(true)
if err != nil {
log.Error("stats: opening a transaction: %s", err)
s.logger.Error("opening transaction", slogutil.KeyError, err)
} else if err = finishTxn(tx, false); err != nil {
// Don't wrap the error since it's informative enough as is.
return err
@@ -513,21 +534,21 @@ func (s *StatsCtx) clear() (err error) {
}
// All active transactions are now closed.
log.Debug("stats: database closed")
s.logger.Debug("database closed")
}
err = os.Remove(s.filename)
if err != nil {
log.Error("stats: %s", err)
s.logger.Error("removing", slogutil.KeyError, err)
}
err = s.openDB()
if err != nil {
log.Error("stats: opening database: %s", err)
s.logger.Error("opening database", slogutil.KeyError, err)
}
// Use defer to unlock the mutex as soon as possible.
defer log.Debug("stats: cleared")
defer s.logger.Debug("cleared")
s.currMu.Lock()
defer s.currMu.Unlock()
@@ -548,7 +569,7 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
// taken into account.
tx, err := db.Begin(true)
if err != nil {
log.Error("stats: opening transaction: %s", err)
s.logger.Error("opening transaction", slogutil.KeyError, err)
return nil, 0
}
@@ -568,7 +589,7 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
units = make([]*unitDB, 0, limit)
firstID := curID - limit + 1
for i := firstID; i != curID; i++ {
u := loadUnitFromDB(tx, i)
u := s.loadUnitFromDB(tx, i)
if u == nil {
u = &unitDB{NResult: make([]uint64, resultLast)}
}
@@ -577,7 +598,7 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
err = finishTxn(tx, false)
if err != nil {
log.Error("stats: %s", err)
s.logger.Error("finishing transaction", slogutil.KeyError, err)
}
if cur != nil {
@@ -585,7 +606,8 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
}
if unitsLen := len(units); unitsLen != int(limit) {
log.Fatalf("loaded %d units whilst the desired number is %d", unitsLen, limit)
// Should not happen.
panic(fmt.Errorf("loaded %d units when the desired number is %d", unitsLen, limit))
}
return units, curID

View File

@@ -8,6 +8,7 @@ import (
"testing"
"time"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
@@ -18,6 +19,7 @@ func TestStats_races(t *testing.T) {
var r uint32
idGen := func() (id uint32) { return atomic.LoadUint32(&r) }
conf := Config{
Logger: slogutil.NewDiscardLogger(),
ShouldCountClient: func([]string) bool { return true },
UnitID: idGen,
Filename: filepath.Join(t.TempDir(), "./stats.db"),
@@ -94,6 +96,7 @@ func TestStatsCtx_FillCollectedStats_daily(t *testing.T) {
)
s, err := New(Config{
Logger: slogutil.NewDiscardLogger(),
ShouldCountClient: func([]string) bool { return true },
Filename: filepath.Join(t.TempDir(), "./stats.db"),
Limit: time.Hour,
@@ -151,6 +154,7 @@ func TestStatsCtx_DataFromUnits_month(t *testing.T) {
const hoursInMonth = 720
s, err := New(Config{
Logger: slogutil.NewDiscardLogger(),
ShouldCountClient: func([]string) bool { return true },
Filename: filepath.Join(t.TempDir(), "./stats.db"),
Limit: time.Hour,

View File

@@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -21,10 +22,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// constUnitID is the UnitIDGenFunc which always return 0.
func constUnitID() (id uint32) { return 0 }
@@ -55,6 +52,7 @@ func TestStats(t *testing.T) {
handlers := map[string]http.Handler{}
conf := stats.Config{
Logger: slogutil.NewDiscardLogger(),
ShouldCountClient: func([]string) bool { return true },
Filename: filepath.Join(t.TempDir(), "stats.db"),
Limit: timeutil.Day,
@@ -171,6 +169,7 @@ func TestLargeNumbers(t *testing.T) {
handlers := map[string]http.Handler{}
conf := stats.Config{
Logger: slogutil.NewDiscardLogger(),
ShouldCountClient: func([]string) bool { return true },
Filename: filepath.Join(t.TempDir(), "stats.db"),
Limit: timeutil.Day,
@@ -222,6 +221,7 @@ func TestShouldCount(t *testing.T) {
require.NoError(t, err)
s, err := stats.New(stats.Config{
Logger: slogutil.NewDiscardLogger(),
Enabled: true,
Filename: filepath.Join(t.TempDir(), "stats.db"),
Limit: timeutil.Day,

View File

@@ -10,7 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"go.etcd.io/bbolt"
"golang.org/x/exp/maps"
)
@@ -277,13 +277,14 @@ func (u *unit) serialize() (udb *unitDB) {
}
}
func loadUnitFromDB(tx *bbolt.Tx, id uint32) (udb *unitDB) {
// loadUnitFromDB loads unit by id from the database.
func (s *StatsCtx) loadUnitFromDB(tx *bbolt.Tx, id uint32) (udb *unitDB) {
bkt := tx.Bucket(idToUnitName(id))
if bkt == nil {
return nil
}
log.Tracef("Loading unit %d", id)
s.logger.Debug("loading unit", "id", id)
var buf bytes.Buffer
buf.Write(bkt.Get([]byte{0}))
@@ -291,7 +292,7 @@ func loadUnitFromDB(tx *bbolt.Tx, id uint32) (udb *unitDB) {
err := gob.NewDecoder(&buf).Decode(udb)
if err != nil {
log.Error("gob Decode: %s", err)
s.logger.Error("gob decode", slogutil.KeyError, err)
return nil
}
@@ -339,8 +340,8 @@ func (u *unit) add(e *Entry) {
}
// flushUnitToDB puts udb to the database at id.
func (udb *unitDB) flushUnitToDB(tx *bbolt.Tx, id uint32) (err error) {
log.Debug("stats: flushing unit with id %d and total of %d", id, udb.NTotal)
func (s *StatsCtx) flushUnitToDB(udb *unitDB, tx *bbolt.Tx, id uint32) (err error) {
s.logger.Debug("flushing unit", "id", id, "req_num", udb.NTotal)
bkt, err := tx.CreateBucketIfNotExists(idToUnitName(id))
if err != nil {

View File

@@ -1,5 +0,0 @@
// Package tools and its main module are a nested internal module containing our
// development tool dependencies.
//
// See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module.
package tools

View File

@@ -1,6 +1,6 @@
module github.com/AdguardTeam/AdGuardHome/internal/tools
go 1.22.4
go 1.23.1
require (
github.com/fzipp/gocyclo v0.6.0
@@ -8,28 +8,58 @@ require (
github.com/gordonklaus/ineffassign v0.1.0
github.com/kisielk/errcheck v1.7.0
github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.20.0
github.com/uudashr/gocognit v1.1.2
golang.org/x/tools v0.22.0
golang.org/x/vuln v1.1.2
honnef.co/go/tools v0.4.7
mvdan.cc/gofumpt v0.6.0
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f
github.com/securego/gosec/v2 v2.21.4
github.com/uudashr/gocognit v1.1.3
golang.org/x/tools v0.25.0
golang.org/x/vuln v1.1.3
honnef.co/go/tools v0.5.1
mvdan.cc/gofumpt v0.7.0
mvdan.cc/unparam v0.0.0-20240917084806-57a3b4290ba3
)
require (
github.com/BurntSushi/toml v1.4.0 // indirect
cloud.google.com/go v0.115.1 // indirect
cloud.google.com/go/ai v0.8.2 // indirect
cloud.google.com/go/auth v0.9.5 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/longrunning v0.6.1 // 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
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/generative-ai-go v0.18.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/s2a-go v0.1.8 // 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.13.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/kyoh86/nolint v0.0.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/exp/typeparams v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/telemetry v0.0.0-20240701175443-4e29c7872ac1 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/telemetry v0.0.0-20240927214544-e9e6960092dd // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/api v0.199.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
google.golang.org/grpc v1.67.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,29 +1,87 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
cloud.google.com/go/ai v0.8.2 h1:LEaQwqBv+k2ybrcdTtCTc9OPZXoEdcQaGrfvDYS6Bnk=
cloud.google.com/go/ai v0.8.2/go.mod h1:Wb3EUUGWwB6yHBaUf/+oxUq/6XbCaU1yh0GrwUS8lr4=
cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw=
cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
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.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc=
cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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=
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
github.com/google/generative-ai-go v0.18.0 h1:6ybg9vOCLcI/UpBBYXOTVgvKmcUKFRNj+2Cj3GnebSo=
github.com/google/generative-ai-go v0.18.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
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/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
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=
@@ -38,91 +96,157 @@ github.com/kyoh86/looppointer v0.2.1 h1:Jx9fnkBj/JrIryBLMTYNTj9rvc2SrPS98Dg0w7fx
github.com/kyoh86/looppointer v0.2.1/go.mod h1:q358WcM8cMWU+5vzqukvaZtnJi1kw/MpRHQm3xvTrjw=
github.com/kyoh86/nolint v0.0.1 h1:GjNxDEkVn2wAxKHtP7iNTrRxytRZ1wXxLV5j4XzGfRU=
github.com/kyoh86/nolint v0.0.1/go.mod h1:1ZiZZ7qqrZ9dZegU96phwVcdQOMKIqRzFJL3ewq9gtI=
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/securego/gosec/v2 v2.20.0 h1:z/d5qp1niWa2avgFyUIglYTYYuGq2LrJwNj1HRVXsqc=
github.com/securego/gosec/v2 v2.20.0/go.mod h1:hkiArbBZLwK1cehBcg3oFWUlYPWTBffPwwJVWChu83o=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI=
github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM=
github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U=
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
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.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/typeparams v0.0.0-20240613232115-7f521ea00fb8 h1:+ZJmEdDFzH5H0CnzOrwgbH3elHctfTecW9X0k2tkn5M=
golang.org/x/exp/typeparams v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 h1:bVwtbF629Xlyxk6xLQq2TDYmqP0uiWaet5LwRebuY0k=
golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240701175443-4e29c7872ac1 h1:jveUVYFLPlIma1aZBg9rrUN+Dqk4e6QbVSGiZGwA/2Y=
golang.org/x/telemetry v0.0.0-20240701175443-4e29c7872ac1/go.mod h1:n38mvGdgc4dA684EC4NwQwoPKSw4jyKw8/DgZHDA1Dk=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240927214544-e9e6960092dd h1:cSUM3UgI7q2QZ4WBwDOGo5eFhZG4eGUkpdFporYHwpQ=
golang.org/x/telemetry v0.0.0-20240927214544-e9e6960092dd/go.mod h1:PsFMgI0jiuY7j+qwXANuh9a/x5kQESTSnRow3gapUyk=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/vuln v1.1.2 h1:UkLxe+kAMcrNBpGrFbU0Mc5l7cX97P2nhy21wx5+Qbk=
golang.org/x/vuln v1.1.2/go.mod h1:2o3fRKD8Uz9AraAL3lwd/grWBv+t+SeJnPcqBUJrY24=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
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/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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs=
google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA=
google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
mvdan.cc/unparam v0.0.0-20240917084806-57a3b4290ba3 h1:YkmTN1n5U60NM02j7TCSWRlW3fqNiuXe/eVXf0dLFN8=
mvdan.cc/unparam v0.0.0-20240917084806-57a3b4290ba3/go.mod h1:z5yboO1sP1Q9pcfvS597TpfbNXQjphDlkCJHzt13ybc=

Some files were not shown because too many files have changed in this diff Show More