all: resync with master
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
195
internal/dhcpsvc/db.go
Normal 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
|
||||
}
|
||||
4
internal/dhcpsvc/db_internal_test.go
Normal file
4
internal/dhcpsvc/db_internal_test.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package dhcpsvc
|
||||
|
||||
// DatabasePerm is the permissions for the test database file.
|
||||
const DatabasePerm = databasePerm
|
||||
@@ -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.
|
||||
|
||||
66
internal/dhcpsvc/dhcpsvc_test.go
Normal file
66
internal/dhcpsvc/dhcpsvc_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
19
internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json
vendored
Normal file
19
internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json
vendored
Normal 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
|
||||
}
|
||||
33
internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json
vendored
Normal file
33
internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json
vendored
Normal 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
|
||||
}
|
||||
26
internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json
vendored
Normal file
26
internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json
vendored
Normal 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
|
||||
}
|
||||
33
internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json
vendored
Normal file
33
internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json
vendored
Normal 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
|
||||
}
|
||||
15
internal/dhcpsvc/testdata/TestServer_Leases/leases.json
vendored
Normal file
15
internal/dhcpsvc/testdata/TestServer_Leases/leases.json
vendored
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "!!!": ` +
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]), ".")
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||
|edgeservices.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||
|
||||
1
internal/filtering/safesearch/rules/ecosia.txt
Normal file
1
internal/filtering/safesearch/rules/ecosia.txt
Normal file
@@ -0,0 +1 @@
|
||||
|www.ecosia.org^$dnsrewrite=NOERROR;CNAME;strict-safe-search.ecosia.org
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -25,6 +25,7 @@ var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
||||
Enabled: true,
|
||||
Bing: true,
|
||||
DuckDuckGo: true,
|
||||
Ecosia: true,
|
||||
Google: true,
|
||||
Pixabay: true,
|
||||
Yandex: true,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -423,6 +423,7 @@ var config = &configuration{
|
||||
Enabled: false,
|
||||
Bing: true,
|
||||
DuckDuckGo: true,
|
||||
Ecosia: true,
|
||||
Google: true,
|
||||
Pixabay: true,
|
||||
Yandex: true,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user