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
375 lines
8.5 KiB
Go
375 lines
8.5 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
|
)
|
|
|
|
// macKey contains MAC as byte array of 6, 8, or 20 bytes.
|
|
type macKey any
|
|
|
|
// macToKey converts mac into key of type macKey, which is used as the key of
|
|
// the [clientIndex.macToUID]. mac must be valid MAC address.
|
|
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))
|
|
}
|
|
}
|
|
|
|
// index stores all information about persistent clients.
|
|
type index struct {
|
|
// nameToUID maps client name to UID.
|
|
nameToUID map[string]UID
|
|
|
|
// clientIDToUID maps ClientID to UID.
|
|
clientIDToUID map[ClientID]UID
|
|
|
|
// ipToUID maps IP address to UID.
|
|
ipToUID map[netip.Addr]UID
|
|
|
|
// macToUID maps MAC address to UID.
|
|
macToUID map[macKey]UID
|
|
|
|
// uidToClient maps UID to the persistent client.
|
|
uidToClient map[UID]*Persistent
|
|
|
|
// subnetToUID maps subnet to UID.
|
|
subnetToUID aghalg.SortedMap[netip.Prefix, UID]
|
|
}
|
|
|
|
// newIndex initializes the new instance of client index.
|
|
func newIndex() (ci *index) {
|
|
return &index{
|
|
nameToUID: map[string]UID{},
|
|
clientIDToUID: map[ClientID]UID{},
|
|
ipToUID: map[netip.Addr]UID{},
|
|
subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare),
|
|
macToUID: map[macKey]UID{},
|
|
uidToClient: map[UID]*Persistent{},
|
|
}
|
|
}
|
|
|
|
// add stores information about a persistent client in the index. c must be
|
|
// non-nil, have a UID, and contain at least one identifier.
|
|
func (ci *index) add(c *Persistent) {
|
|
if (c.UID == UID{}) {
|
|
panic("client must contain uid")
|
|
}
|
|
|
|
ci.nameToUID[c.Name] = c.UID
|
|
|
|
for _, id := range c.ClientIDs {
|
|
ci.clientIDToUID[id] = c.UID
|
|
}
|
|
|
|
for _, ip := range c.IPs {
|
|
ci.ipToUID[ip] = c.UID
|
|
}
|
|
|
|
for _, pref := range c.Subnets {
|
|
ci.subnetToUID.Set(pref, c.UID)
|
|
}
|
|
|
|
for _, mac := range c.MACs {
|
|
k := macToKey(mac)
|
|
ci.macToUID[k] = c.UID
|
|
}
|
|
|
|
ci.uidToClient[c.UID] = c
|
|
}
|
|
|
|
// clashesUID returns existing persistent client with the same UID as c. Note
|
|
// that this is only possible when configuration contains duplicate fields.
|
|
func (ci *index) clashesUID(c *Persistent) (err error) {
|
|
p, ok := ci.uidToClient[c.UID]
|
|
if ok {
|
|
return fmt.Errorf("another client %q uses the same uid", p.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// clashes returns an error if the index contains a different persistent client
|
|
// with at least a single identifier contained by c. c must be non-nil.
|
|
func (ci *index) clashes(c *Persistent) (err error) {
|
|
if p := ci.clashesName(c); p != nil {
|
|
return fmt.Errorf("another client uses the same name %q", p.Name)
|
|
}
|
|
|
|
for _, id := range c.ClientIDs {
|
|
existing, ok := ci.clientIDToUID[id]
|
|
if ok && existing != c.UID {
|
|
p := ci.uidToClient[existing]
|
|
|
|
return fmt.Errorf("another client %q uses the same ClientID %q", p.Name, id)
|
|
}
|
|
}
|
|
|
|
p, ip := ci.clashesIP(c)
|
|
if p != nil {
|
|
return fmt.Errorf("another client %q uses the same IP %q", p.Name, ip)
|
|
}
|
|
|
|
p, s := ci.clashesSubnet(c)
|
|
if p != nil {
|
|
return fmt.Errorf("another client %q uses the same subnet %q", p.Name, s)
|
|
}
|
|
|
|
p, mac := ci.clashesMAC(c)
|
|
if p != nil {
|
|
return fmt.Errorf("another client %q uses the same MAC %q", p.Name, mac)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// clashesName returns existing persistent client with the same name as c or
|
|
// nil. c must be non-nil.
|
|
func (ci *index) clashesName(c *Persistent) (existing *Persistent) {
|
|
existing, ok := ci.findByName(c.Name)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if existing.UID != c.UID {
|
|
return existing
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// clashesIP returns a previous client with the same IP address as c. c must be
|
|
// non-nil.
|
|
func (ci *index) clashesIP(c *Persistent) (p *Persistent, ip netip.Addr) {
|
|
for _, ip := range c.IPs {
|
|
existing, ok := ci.ipToUID[ip]
|
|
if ok && existing != c.UID {
|
|
return ci.uidToClient[existing], ip
|
|
}
|
|
}
|
|
|
|
return nil, netip.Addr{}
|
|
}
|
|
|
|
// clashesSubnet returns a previous client with the same subnet as c. c must be
|
|
// non-nil.
|
|
func (ci *index) clashesSubnet(c *Persistent) (p *Persistent, s netip.Prefix) {
|
|
for _, s = range c.Subnets {
|
|
var existing UID
|
|
var ok bool
|
|
|
|
ci.subnetToUID.Range(func(p netip.Prefix, uid UID) (cont bool) {
|
|
if s == p {
|
|
existing = uid
|
|
ok = true
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if ok && existing != c.UID {
|
|
return ci.uidToClient[existing], s
|
|
}
|
|
}
|
|
|
|
return nil, netip.Prefix{}
|
|
}
|
|
|
|
// clashesMAC returns a previous client with the same MAC address as c. c must
|
|
// be non-nil.
|
|
func (ci *index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr) {
|
|
for _, mac = range c.MACs {
|
|
k := macToKey(mac)
|
|
existing, ok := ci.macToUID[k]
|
|
if ok && existing != c.UID {
|
|
return ci.uidToClient[existing], mac
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// find finds persistent client by string representation of the ClientID, IP
|
|
// address, or MAC.
|
|
func (ci *index) find(id string) (c *Persistent, ok bool) {
|
|
c, ok = ci.findByClientID(ClientID(id))
|
|
if ok {
|
|
return c, true
|
|
}
|
|
|
|
ip, err := netip.ParseAddr(id)
|
|
if err == nil {
|
|
// MAC addresses can be successfully parsed as IP addresses.
|
|
c, ok = ci.findByIP(ip)
|
|
if ok {
|
|
return c, true
|
|
}
|
|
}
|
|
|
|
mac, err := net.ParseMAC(id)
|
|
if err == nil {
|
|
return ci.findByMAC(mac)
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// findByClientID finds persistent client by ClientID.
|
|
func (ci *index) findByClientID(clientID ClientID) (c *Persistent, ok bool) {
|
|
uid, ok := ci.clientIDToUID[clientID]
|
|
if ok {
|
|
return ci.uidToClient[uid], true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// findByName finds persistent client by name.
|
|
func (ci *index) findByName(name string) (c *Persistent, found bool) {
|
|
uid, found := ci.nameToUID[name]
|
|
if found {
|
|
return ci.uidToClient[uid], true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// findByIP finds persistent client by IP address.
|
|
func (ci *index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
|
uid, found := ci.ipToUID[ip]
|
|
if found {
|
|
return ci.uidToClient[uid], true
|
|
}
|
|
|
|
ipWithoutZone := ip.WithZone("")
|
|
ci.subnetToUID.Range(func(pref netip.Prefix, id UID) (cont bool) {
|
|
// Remove zone before checking because prefixes strip zones.
|
|
if pref.Contains(ipWithoutZone) {
|
|
uid, found = id, true
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if found {
|
|
return ci.uidToClient[uid], true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// findByCIDR searches for a persistent client with the provided subnet as an
|
|
// identifier. Note that this function looks for an exact match of subnets,
|
|
// rather than checking if one subnet contains another.
|
|
func (ci *index) findByCIDR(subnet netip.Prefix) (c *Persistent, ok bool) {
|
|
var uid UID
|
|
for pref, id := range ci.subnetToUID.Range {
|
|
if subnet == pref {
|
|
uid, ok = id, true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if ok {
|
|
return ci.uidToClient[uid], true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// findByMAC finds persistent client by MAC.
|
|
func (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
|
|
k := macToKey(mac)
|
|
uid, found := ci.macToUID[k]
|
|
if found {
|
|
return ci.uidToClient[uid], true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// findByIPWithoutZone finds a persistent client by IP address without zone. It
|
|
// strips the IPv6 zone index from the stored IP addresses before comparing,
|
|
// because querylog entries don't have it. See TODO on [querylog.logEntry.IP].
|
|
//
|
|
// Note that multiple clients can have the same IP address with different zones.
|
|
// Therefore, the result of this method is indeterminate.
|
|
func (ci *index) findByIPWithoutZone(ip netip.Addr) (c *Persistent) {
|
|
if (ip == netip.Addr{}) {
|
|
return nil
|
|
}
|
|
|
|
for addr, uid := range ci.ipToUID {
|
|
if addr.WithZone("") == ip {
|
|
return ci.uidToClient[uid]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// remove removes information about persistent client from the index. c must be
|
|
// non-nil.
|
|
func (ci *index) remove(c *Persistent) {
|
|
delete(ci.nameToUID, c.Name)
|
|
|
|
for _, id := range c.ClientIDs {
|
|
delete(ci.clientIDToUID, id)
|
|
}
|
|
|
|
for _, ip := range c.IPs {
|
|
delete(ci.ipToUID, ip)
|
|
}
|
|
|
|
for _, pref := range c.Subnets {
|
|
ci.subnetToUID.Del(pref)
|
|
}
|
|
|
|
for _, mac := range c.MACs {
|
|
k := macToKey(mac)
|
|
delete(ci.macToUID, k)
|
|
}
|
|
|
|
delete(ci.uidToClient, c.UID)
|
|
}
|
|
|
|
// size returns the number of persistent clients.
|
|
func (ci *index) size() (n int) {
|
|
return len(ci.uidToClient)
|
|
}
|
|
|
|
// rangeByName is like [Index.Range] but sorts the persistent clients by name
|
|
// before iterating ensuring a predictable order.
|
|
func (ci *index) rangeByName(f func(c *Persistent) (cont bool)) {
|
|
clients := slices.SortedStableFunc(
|
|
maps.Values(ci.uidToClient),
|
|
func(a, b *Persistent) (res int) {
|
|
return strings.Compare(a.Name, b.Name)
|
|
},
|
|
)
|
|
|
|
for _, c := range clients {
|
|
if !f(c) {
|
|
break
|
|
}
|
|
}
|
|
}
|