AdGuardHome/internal/client/persistent.go
Dimitry Kolyshev 762ef4a6db Pull request: AG-31778-fix-safesearch-https
Squashed commit of the following:

commit 85ea3d985e83209e3b49119959aedd330df24d23
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Apr 18 15:19:38 2024 +0200

    all: imp docs

commit b0695daddbcf191454c5e829ca4d19def8ddacbf
Merge: a79f98f2f 48c6242a7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Apr 17 11:06:49 2024 +0200

    Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https

    # Conflicts:
    #	CHANGELOG.md

commit a79f98f2f215a4a79ca4d186c0da33db936429dc
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Apr 17 11:05:34 2024 +0200

    dnsforward: imp code

commit b901a1169cc78313298d70cce770cd1523ccbf9f
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Apr 16 11:03:52 2024 +0200

    dnsforward: imp code

commit fb6e66971b1b984147ec400ceaff856e7b5710c7
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Apr 16 10:08:51 2024 +0200

    all: safesearch rewrites

commit 88add21831fff7e04539f5dd299832883a6f3995
Merge: b78ad8f74 201ac73cf
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Apr 16 09:43:20 2024 +0200

    Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https

    # Conflicts:
    #	CHANGELOG.md

commit b78ad8f748c7fa52533e0541cae16bd51c201370
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Apr 12 13:34:39 2024 +0200

    all: safesearch rewrites

commit fb3efbb053242c537ca872542006917b8e8ac1ff
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Apr 11 13:15:37 2024 +0200

    safesearch: imp code

commit 1193c704f4d30be4a2cc66e84a31c9a6020ab269
Merge: 14e823d7c ff7c715c5
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Apr 11 13:13:44 2024 +0200

    Merge remote-tracking branch 'origin/master' into AG-31778-fix-safesearch-https

    # Conflicts:
    #	CHANGELOG.md

commit 14e823d7cc13c275c2ed04704883a94b95e29963
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Apr 11 13:11:43 2024 +0200

    all: safesearch https

commit cd403a2897ae56a9059a78f24b104af5805d84ab
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Apr 11 12:09:27 2024 +0200

    Revert "all: safesearch https"

    This reverts commit 1c9564b9b4db70f85b2f827cc06b65d2b67b08b1.

commit 1c9564b9b4db70f85b2f827cc06b65d2b67b08b1
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Apr 10 12:41:47 2024 +0200

    all: safesearch https

commit 5f42688fbab566973acc8dc414a992819492a9ac
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Apr 10 09:22:30 2024 +0200

    filtering: imp code

commit eb9bd9f47cd71cafe8eee4698a8a0d5d25dea3d3
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Apr 10 09:19:22 2024 +0200

    all: changelog

commit 0c77c705a942fe83d3809a7efbc8a6baf5886762
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Apr 10 08:55:22 2024 +0200

    safesearch: imp tests

commit 492a93fbb5ff54678e22a15577f509b2327c2ebe
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Apr 9 14:45:16 2024 +0200

    all: changelog

commit a665e7246d11503c47d48ccc714e6862f764e930
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Apr 9 14:41:24 2024 +0200

    safesearch: https req
2024-04-22 10:48:26 +03:00

286 lines
7 KiB
Go

package client
import (
"encoding"
"fmt"
"net"
"net/netip"
"slices"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"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 {
// UpstreamConfig is the custom upstream configuration for this client. If
// it's nil, it has not been initialized yet. If it's non-nil and empty,
// there are no valid upstreams. If it's non-nil and non-empty, these
// upstream must be used.
UpstreamConfig *proxy.CustomUpstreamConfig
SafeSearch 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
// TODO(d.kolyshev): Make SafeSearchConf a pointer.
SafeSearchConf filtering.SafeSearchConfig
}
// SetTags sets the tags if they are known, otherwise logs an unknown tag.
func (c *Persistent) SetTags(tags []string, known *container.MapSet[string]) {
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 *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, strings.ToLower(id))
return nil
}
// ValidateClientID returns an error if id is not a valid ClientID.
//
// TODO(s.chzhen): It's an exact copy of the [dnsforward.ValidateClientID] to
// avoid the import cycle. Remove it.
func ValidateClientID(id string) (err error) {
err = netutil.ValidateHostnameLabel(id)
if err != nil {
// Replace the domain name label wrapper with our own.
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
}
return nil
}
// IDs returns a list of client IDs containing at least one element.
func (c *Persistent) 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 *Persistent) 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 *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
}
// CloseUpstreams closes the client-specific upstream config of c if any.
func (c *Persistent) CloseUpstreams() (err error) {
if c.UpstreamConfig != nil {
if err = c.UpstreamConfig.Close(); err != nil {
return fmt.Errorf("closing upstreams of client %q: %w", c.Name, err)
}
}
return nil
}
// SetSafeSearch initializes and sets the safe search filter for this client.
func (c *Persistent) 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
}