Merge in DNS/adguard-home from AGDNS-2750-find-client to master Squashed commit of the following: commit 98f1a8ca4622b6f502a5092273b9724203fe0bd8 Merge: 9270222d84ccc2a213Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Apr 23 17:53:20 2025 +0300 Merge branch 'master' into AGDNS-2750-find-client commit 9270222d8e9e03038e9434b54496cbb6164463cd Merge: 6468ceec8c7c62ad3bAuthor: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Apr 21 19:40:58 2025 +0300 Merge branch 'master' into AGDNS-2750-find-client commit 6468ceec82d30084771a53ff6720a8c11c68bf2f Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Apr 21 19:40:52 2025 +0300 home: imp docs commit 3fd4735a0d6db4fdf2d46f3da9794a687fdcaa8b Merge: 1311a5869a8fdf1c55Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Apr 18 19:43:36 2025 +0300 Merge branch 'master' into AGDNS-2750-find-client commit 1311a58695de00f20c9704378ee6e964a44d1c59 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Apr 18 19:42:41 2025 +0300 home: imp code commit b1f2c4c883c9476c5135140abac31f8ae6609b4f Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Apr 16 16:47:59 2025 +0300 home: imp code commit d0a5abd66587c1ad602c2ccf6c8a45a3dfe39a5c Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Apr 15 14:58:31 2025 +0300 client: imp naming commit 5accdca325551237f003f1c416891b488fe5290b Merge: 6a00232f74d258972dAuthor: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Apr 14 19:40:40 2025 +0300 Merge branch 'master' into AGDNS-2750-find-client commit 6a00232f76a0fe5ce781aa01637b6e04ace7250d Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Apr 14 19:30:32 2025 +0300 home: imp code commit 8633886457c6aab75f5676494b1f49d9811e9ab9 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Apr 11 15:29:25 2025 +0300 all: imp code commit d6f16879e7b054a5ffac59131d2a6eff1da659c0 Merge: 58236fdec6d282ae71Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Apr 10 21:35:23 2025 +0300 Merge branch 'master' into AGDNS-2750-find-client commit 58236fdec5b64e83a44680ff8a89badc18ec81f1 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Apr 10 21:23:01 2025 +0300 all: upd ci commit 3c4d946d7970987677d4ac984394e18987a29f9a Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Apr 10 21:16:03 2025 +0300 all: upd go commit cc1c97734506a9ffbe70fd3c676284e58a21ba46 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Apr 10 20:58:56 2025 +0300 all: imp code commit 8f061c933152481a4c80eef2af575efd4919d82b Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Apr 9 16:49:11 2025 +0300 all: imp docs commit 8d19355f1c519211a56cec3f23d527922d4f2ee0 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Apr 7 21:35:06 2025 +0300 all: imp code commit f1e853f57e5d54d13bedcdab4f8e21e112f3a356 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Apr 2 14:57:40 2025 +0300 all: imp code commit 6a6ac7f899f29ddc90a583c80562233e646ba1d6 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Apr 1 19:51:56 2025 +0300 client: imp tests commit 52040ee7393d0483c682f2f37d7b70f12f9cf621 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Apr 1 19:28:18 2025 +0300 all: imp code commit 1e09208dbd2d35c3f6b2ade169324e23d1a643a5 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Mar 26 15:33:02 2025 +0300 all: imp code ... and 2 more commits
299 lines
7.5 KiB
Go
299 lines
7.5 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// UID is the type for the unique IDs of persistent clients.
|
|
type UID uuid.UUID
|
|
|
|
// NewUID returns a new persistent client UID. Any error returned is an error
|
|
// from the cryptographic randomness reader.
|
|
func NewUID() (uid UID, err error) {
|
|
uuidv7, err := uuid.NewV7()
|
|
|
|
return UID(uuidv7), err
|
|
}
|
|
|
|
// MustNewUID is a wrapper around [NewUID] that panics if there is an error.
|
|
func MustNewUID() (uid UID) {
|
|
uid, err := NewUID()
|
|
if err != nil {
|
|
panic(fmt.Errorf("unexpected uuidv7 error: %w", err))
|
|
}
|
|
|
|
return uid
|
|
}
|
|
|
|
// type check
|
|
var _ encoding.TextMarshaler = UID{}
|
|
|
|
// MarshalText implements the [encoding.TextMarshaler] for UID.
|
|
func (uid UID) MarshalText() ([]byte, error) {
|
|
return uuid.UUID(uid).MarshalText()
|
|
}
|
|
|
|
// type check
|
|
var _ encoding.TextUnmarshaler = (*UID)(nil)
|
|
|
|
// UnmarshalText implements the [encoding.TextUnmarshaler] interface for UID.
|
|
func (uid *UID) UnmarshalText(data []byte) error {
|
|
return (*uuid.UUID)(uid).UnmarshalText(data)
|
|
}
|
|
|
|
// Persistent contains information about persistent clients.
|
|
type Persistent struct {
|
|
// SafeSearch handles search engine hosts rewrites.
|
|
SafeSearch filtering.SafeSearch
|
|
|
|
// BlockedServices is the configuration of blocked services of a client. It
|
|
// must not be nil after initialization.
|
|
BlockedServices *filtering.BlockedServices
|
|
|
|
// Name of the persistent client. Must not be empty.
|
|
Name string
|
|
|
|
// Tags is a list of client tags that categorize the client.
|
|
Tags []string
|
|
|
|
// Upstreams is a list of custom upstream DNS servers for the client.
|
|
Upstreams []string
|
|
|
|
// IPs is a list of IP addresses that identify the client. The client must
|
|
// have at least one ID (IP, subnet, MAC, or ClientID).
|
|
IPs []netip.Addr
|
|
|
|
// Subnets identifying the client. The client must have at least one ID
|
|
// (IP, subnet, MAC, or ClientID).
|
|
//
|
|
// TODO(s.chzhen): Use netutil.Prefix.
|
|
Subnets []netip.Prefix
|
|
|
|
// MACs identifying the client. The client must have at least one ID (IP,
|
|
// subnet, MAC, or ClientID).
|
|
MACs []net.HardwareAddr
|
|
|
|
// ClientIDs identifying the client. The client must have at least one ID
|
|
// (IP, subnet, MAC, or ClientID).
|
|
ClientIDs []ClientID
|
|
|
|
// UID is the unique identifier of the persistent client.
|
|
UID UID
|
|
|
|
// UpstreamsCacheSize is the cache size for custom upstreams.
|
|
UpstreamsCacheSize uint32
|
|
|
|
// UpstreamsCacheEnabled specifies whether custom upstreams are used.
|
|
UpstreamsCacheEnabled bool
|
|
|
|
// UseOwnSettings specifies whether custom filtering settings are used.
|
|
UseOwnSettings bool
|
|
|
|
// FilteringEnabled specifies whether filtering is enabled.
|
|
FilteringEnabled bool
|
|
|
|
// SafeBrowsingEnabled specifies whether safe browsing is enabled.
|
|
SafeBrowsingEnabled bool
|
|
|
|
// ParentalEnabled specifies whether parental control is enabled.
|
|
ParentalEnabled bool
|
|
|
|
// UseOwnBlockedServices specifies whether custom services are blocked.
|
|
UseOwnBlockedServices bool
|
|
|
|
// IgnoreQueryLog specifies whether the client requests are logged.
|
|
IgnoreQueryLog bool
|
|
|
|
// IgnoreStatistics specifies whether the client requests are counted.
|
|
IgnoreStatistics bool
|
|
|
|
// SafeSearchConf is the safe search filtering configuration.
|
|
//
|
|
// TODO(d.kolyshev): Make SafeSearchConf a pointer.
|
|
SafeSearchConf filtering.SafeSearchConfig
|
|
}
|
|
|
|
// validate returns an error if persistent client information contains errors.
|
|
// allTags must be sorted.
|
|
func (c *Persistent) validate(ctx context.Context, l *slog.Logger, allTags []string) (err error) {
|
|
switch {
|
|
case c.Name == "":
|
|
return errors.Error("empty name")
|
|
case c.idendifiersLen() == 0:
|
|
return errors.Error("id required")
|
|
case c.UID == UID{}:
|
|
return errors.Error("uid required")
|
|
}
|
|
|
|
conf, err := proxy.ParseUpstreamsConfig(c.Upstreams, &upstream.Options{})
|
|
if err != nil {
|
|
return fmt.Errorf("invalid upstream servers: %w", err)
|
|
}
|
|
|
|
err = conf.Close()
|
|
if err != nil {
|
|
l.ErrorContext(ctx, "client: closing upstream config", slogutil.KeyError, err)
|
|
}
|
|
|
|
for _, t := range c.Tags {
|
|
_, ok := slices.BinarySearch(allTags, t)
|
|
if !ok {
|
|
return fmt.Errorf("invalid tag: %q", t)
|
|
}
|
|
}
|
|
|
|
// TODO(s.chzhen): Move to the constructor.
|
|
slices.Sort(c.Tags)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetIDs parses a list of strings into typed fields and returns an error if
|
|
// there is one.
|
|
func (c *Persistent) SetIDs(ids []string) (err error) {
|
|
for _, id := range ids {
|
|
err = c.setID(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
slices.SortFunc(c.IPs, netip.Addr.Compare)
|
|
|
|
// TODO(s.chzhen): Use netip.PrefixCompare in Go 1.23.
|
|
slices.SortFunc(c.Subnets, subnetCompare)
|
|
slices.SortFunc(c.MACs, slices.Compare[net.HardwareAddr])
|
|
slices.Sort(c.ClientIDs)
|
|
|
|
return nil
|
|
}
|
|
|
|
// subnetCompare is a comparison function for the two subnets. It returns -1 if
|
|
// x sorts before y, 1 if x sorts after y, and 0 if their relative sorting
|
|
// position is the same.
|
|
func subnetCompare(x, y netip.Prefix) (cmp int) {
|
|
if x == y {
|
|
return 0
|
|
}
|
|
|
|
xAddr, xBits := x.Addr(), x.Bits()
|
|
yAddr, yBits := y.Addr(), y.Bits()
|
|
if xBits == yBits {
|
|
return xAddr.Compare(yAddr)
|
|
}
|
|
|
|
if xBits > yBits {
|
|
return -1
|
|
} else {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// setID parses id into typed field if there is no error.
|
|
func (c *Persistent) setID(id string) (err error) {
|
|
if id == "" {
|
|
return errors.Error("clientid is empty")
|
|
}
|
|
|
|
var ip netip.Addr
|
|
if ip, err = netip.ParseAddr(id); err == nil {
|
|
c.IPs = append(c.IPs, ip)
|
|
|
|
return nil
|
|
}
|
|
|
|
var subnet netip.Prefix
|
|
if subnet, err = netip.ParsePrefix(id); err == nil {
|
|
c.Subnets = append(c.Subnets, subnet)
|
|
|
|
return nil
|
|
}
|
|
|
|
var mac net.HardwareAddr
|
|
if mac, err = net.ParseMAC(id); err == nil {
|
|
c.MACs = append(c.MACs, mac)
|
|
|
|
return nil
|
|
}
|
|
|
|
err = ValidateClientID(id)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
c.ClientIDs = append(c.ClientIDs, ClientID(strings.ToLower(id)))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Identifiers returns a list of client identifiers containing at least one
|
|
// element.
|
|
func (c *Persistent) Identifiers() (ids []string) {
|
|
ids = make([]string, 0, c.idendifiersLen())
|
|
|
|
for _, ip := range c.IPs {
|
|
ids = append(ids, ip.String())
|
|
}
|
|
|
|
for _, subnet := range c.Subnets {
|
|
ids = append(ids, subnet.String())
|
|
}
|
|
|
|
for _, mac := range c.MACs {
|
|
ids = append(ids, mac.String())
|
|
}
|
|
|
|
for _, cid := range c.ClientIDs {
|
|
ids = append(ids, string(cid))
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
// identifiersLen returns the number of client identifiers.
|
|
func (c *Persistent) idendifiersLen() (n int) {
|
|
return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)
|
|
}
|
|
|
|
// EqualIDs returns true if the ids of the current and previous clients are the
|
|
// same.
|
|
func (c *Persistent) EqualIDs(prev *Persistent) (equal bool) {
|
|
return slices.Equal(c.IPs, prev.IPs) &&
|
|
slices.Equal(c.Subnets, prev.Subnets) &&
|
|
slices.EqualFunc(c.MACs, prev.MACs, slices.Equal[net.HardwareAddr]) &&
|
|
slices.Equal(c.ClientIDs, prev.ClientIDs)
|
|
}
|
|
|
|
// ShallowClone returns a deep copy of the client, except upstreamConfig,
|
|
// safeSearchConf, SafeSearch fields, because it's difficult to copy them.
|
|
func (c *Persistent) ShallowClone() (clone *Persistent) {
|
|
clone = &Persistent{}
|
|
*clone = *c
|
|
|
|
clone.BlockedServices = c.BlockedServices.Clone()
|
|
clone.Tags = slices.Clone(c.Tags)
|
|
clone.Upstreams = slices.Clone(c.Upstreams)
|
|
|
|
clone.IPs = slices.Clone(c.IPs)
|
|
clone.Subnets = slices.Clone(c.Subnets)
|
|
clone.MACs = slices.Clone(c.MACs)
|
|
clone.ClientIDs = slices.Clone(c.ClientIDs)
|
|
|
|
return clone
|
|
}
|