mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-03-28 04:59:06 +03:00
Squashed commit of the following:
commit 37e33ec761cfa30164125af2c5bb40789412355e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Feb 14 15:25:25 2024 +0300
aghalg: imp code
commit 6b2f09a44298b474ec1bdf3d027fb4941d2f7bea
Merge: b8ea924aa 37736289e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Feb 14 15:04:59 2024 +0300
Merge branch 'master' into AG-27492-client-persistent-storage
commit b8ea924aa7ed4c052760a6068f945d83d184e7e3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Tue Feb 13 19:07:52 2024 +0300
home: imp tests
commit aa6fec03b1a1ead96bc76919b7ad51ae19626633
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Tue Feb 13 14:54:28 2024 +0300
home: imp docs
commit 10637fdec47d0b035cf5c7949ddcd9ec564851a3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Thu Feb 8 20:16:11 2024 +0300
all: imp code
commit b45c7d868ddb1be73e119b3260e2a866d57baa91
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Feb 7 19:15:11 2024 +0300
aghalg: add tests
commit 7abe33dbaa7221ddbc8b7d802dbfa7f951d90cf8
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Tue Feb 6 20:50:22 2024 +0300
all: imp code, tests
commit 4a44e993c9bd393d2cb9853108eae1ad91e64402
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Thu Feb 1 14:59:11 2024 +0300
all: persistent client index
commit 66b16e216e03e9f3d5e69496a89b18a9d732b564
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Wed Jan 31 15:06:05 2024 +0300
aghalg: ordered map
271 lines
6.7 KiB
Go
271 lines
6.7 KiB
Go
package home
|
|
|
|
import (
|
|
"encoding"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/AdguardTeam/golibs/stringutil"
|
|
"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)
|
|
}
|
|
|
|
// persistentClient contains information about persistent clients.
|
|
type persistentClient struct {
|
|
// upstreamConfig is the custom upstream configuration for this client. If
|
|
// it's nil, it has not been initialized yet. If it's non-nil and empty,
|
|
// there are no valid upstreams. If it's non-nil and non-empty, these
|
|
// upstream must be used.
|
|
upstreamConfig *proxy.CustomUpstreamConfig
|
|
|
|
// TODO(d.kolyshev): Make safeSearchConf a pointer.
|
|
safeSearchConf filtering.SafeSearchConfig
|
|
SafeSearch filtering.SafeSearch
|
|
|
|
// BlockedServices is the configuration of blocked services of a client.
|
|
BlockedServices *filtering.BlockedServices
|
|
|
|
Name string
|
|
|
|
Tags []string
|
|
Upstreams []string
|
|
|
|
IPs []netip.Addr
|
|
// TODO(s.chzhen): Use netutil.Prefix.
|
|
Subnets []netip.Prefix
|
|
MACs []net.HardwareAddr
|
|
ClientIDs []string
|
|
|
|
// UID is the unique identifier of the persistent client.
|
|
UID UID
|
|
|
|
UpstreamsCacheSize uint32
|
|
UpstreamsCacheEnabled bool
|
|
|
|
UseOwnSettings bool
|
|
FilteringEnabled bool
|
|
SafeBrowsingEnabled bool
|
|
ParentalEnabled bool
|
|
UseOwnBlockedServices bool
|
|
IgnoreQueryLog bool
|
|
IgnoreStatistics bool
|
|
}
|
|
|
|
// setTags sets the tags if they are known, otherwise logs an unknown tag.
|
|
func (c *persistentClient) setTags(tags []string, known *stringutil.Set) {
|
|
for _, t := range tags {
|
|
if !known.Has(t) {
|
|
log.Info("skipping unknown tag %q", t)
|
|
|
|
continue
|
|
}
|
|
|
|
c.Tags = append(c.Tags, t)
|
|
}
|
|
|
|
slices.Sort(c.Tags)
|
|
}
|
|
|
|
// setIDs parses a list of strings into typed fields and returns an error if
|
|
// there is one.
|
|
func (c *persistentClient) 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 *persistentClient) 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 = dnsforward.ValidateClientID(id)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
c.ClientIDs = append(c.ClientIDs, strings.ToLower(id))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ids returns a list of client ids containing at least one element.
|
|
func (c *persistentClient) ids() (ids []string) {
|
|
ids = make([]string, 0, c.idsLen())
|
|
|
|
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())
|
|
}
|
|
|
|
return append(ids, c.ClientIDs...)
|
|
}
|
|
|
|
// idsLen returns a length of client ids.
|
|
func (c *persistentClient) idsLen() (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 *persistentClient) equalIDs(prev *persistentClient) (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 *persistentClient) shallowClone() (clone *persistentClient) {
|
|
clone = &persistentClient{}
|
|
*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
|
|
}
|
|
|
|
// closeUpstreams closes the client-specific upstream config of c if any.
|
|
func (c *persistentClient) closeUpstreams() (err error) {
|
|
if c.upstreamConfig != nil {
|
|
if err = c.upstreamConfig.Close(); err != nil {
|
|
return fmt.Errorf("closing upstreams of client %q: %w", c.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setSafeSearch initializes and sets the safe search filter for this client.
|
|
func (c *persistentClient) setSafeSearch(
|
|
conf filtering.SafeSearchConfig,
|
|
cacheSize uint,
|
|
cacheTTL time.Duration,
|
|
) (err error) {
|
|
ss, err := safesearch.NewDefault(conf, fmt.Sprintf("client %q", c.Name), cacheSize, cacheTTL)
|
|
if err != nil {
|
|
// Don't wrap the error, because it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
c.SafeSearch = ss
|
|
|
|
return nil
|
|
}
|