Skip to content

Commit

Permalink
Pull request 2208: AG-27492-client-persistent-list
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 1b1a21b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 8 17:32:38 2024 +0300

    client: imp tests

commit 7e6d171
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 8 17:27:00 2024 +0300

    client: imp tests

commit 5e4cd2b
Merge: 7faddd8 1a62ce4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 8 15:57:33 2024 +0300

    Merge branch 'master' into AG-27492-client-persistent-list

commit 7faddd8
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 6 20:55:43 2024 +0300

    client: imp code

commit 54212e9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 6 20:24:18 2024 +0300

    all: imp code

commit 3f23c9a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 6 17:07:40 2024 +0300

    home: imp tests

commit 39b99fc
Merge: 76469ac 17c4eeb
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 6 16:39:56 2024 +0300

    Merge branch 'master' into AG-27492-client-persistent-list

commit 76469ac
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon May 6 14:36:22 2024 +0300

    home: imp naming

commit 4e4aa58
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu May 2 19:50:45 2024 +0300

    client: imp docs

commit bf5c23a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu May 2 19:40:53 2024 +0300

    home: add tests

commit c6cdba7
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 24 14:21:44 2024 +0300

    all: add tests

commit 1fc43cb
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 19 19:19:48 2024 +0300

    all: add tests

commit ccc423b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 19 15:37:15 2024 +0300

    all: client persistent list
  • Loading branch information
schzhn committed May 13, 2024
1 parent 1a62ce4 commit 71c44fa
Show file tree
Hide file tree
Showing 7 changed files with 634 additions and 151 deletions.
128 changes: 107 additions & 21 deletions internal/client/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"fmt"
"net"
"net/netip"
"slices"
"strings"

"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/golibs/errors"
"golang.org/x/exp/maps"
)

// macKey contains MAC as byte array of 6, 8, or 20 bytes.
Expand All @@ -29,6 +32,9 @@ func macToKey(mac net.HardwareAddr) (key macKey) {

// Index stores all information about persistent clients.
type Index struct {
// nameToUID maps client name to UID.
nameToUID map[string]UID

// clientIDToUID maps client ID to UID.
clientIDToUID map[string]UID

Expand All @@ -48,6 +54,7 @@ type Index struct {
// NewIndex initializes the new instance of client index.
func NewIndex() (ci *Index) {
return &Index{
nameToUID: map[string]UID{},
clientIDToUID: map[string]UID{},
ipToUID: map[netip.Addr]UID{},
subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare),
Expand All @@ -63,6 +70,8 @@ func (ci *Index) Add(c *Persistent) {
panic("client must contain uid")
}

ci.nameToUID[c.Name] = c.UID

for _, id := range c.ClientIDs {
ci.clientIDToUID[id] = c.UID
}
Expand All @@ -83,21 +92,26 @@ func (ci *Index) Add(c *Persistent) {
ci.uidToClient[c.UID] = c
}

// ErrDuplicateUID is an error returned by [Index.Clashes] when adding a
// persistent client with a UID that already exists in an index.
const ErrDuplicateUID errors.Error = "duplicate uid"
// 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) {
_, ok := ci.uidToClient[c.UID]
if ok {
return ErrDuplicateUID
if p := ci.clashesName(c); p != nil {
return fmt.Errorf("another client uses the same name %q", p.Name)
}

for _, id := range c.ClientIDs {
var existing UID
existing, ok = ci.clientIDToUID[id]
existing, ok := ci.clientIDToUID[id]
if ok && existing != c.UID {
p := ci.uidToClient[existing]

Expand All @@ -123,6 +137,21 @@ func (ci *Index) Clashes(c *Persistent) (err error) {
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) {
Expand Down Expand Up @@ -195,13 +224,23 @@ func (ci *Index) Find(id string) (c *Persistent, ok bool) {

mac, err := net.ParseMAC(id)
if err == nil {
return ci.findByMAC(mac)
return ci.FindByMAC(mac)
}

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
}

// find finds persistent client by IP address.
// 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 {
Expand All @@ -227,6 +266,17 @@ func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
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].
Expand All @@ -247,20 +297,11 @@ func (ci *Index) FindByIPWithoutZone(ip netip.Addr) (c *Persistent) {
return nil
}

// find 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
}

// Delete removes information about persistent client from the index. c must be
// non-nil.
func (ci *Index) Delete(c *Persistent) {
delete(ci.nameToUID, c.Name)

for _, id := range c.ClientIDs {
delete(ci.clientIDToUID, id)
}
Expand All @@ -280,3 +321,48 @@ func (ci *Index) Delete(c *Persistent) {

delete(ci.uidToClient, c.UID)
}

// Size returns the number of persistent clients.
func (ci *Index) Size() (n int) {
return len(ci.uidToClient)
}

// Range calls f for each persistent client, unless cont is false. The order is
// undefined.
func (ci *Index) Range(f func(c *Persistent) (cont bool)) {
for _, c := range ci.uidToClient {
if !f(c) {
return
}
}
}

// 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)) {
cs := maps.Values(ci.uidToClient)
slices.SortFunc(cs, func(a, b *Persistent) (n int) {
return strings.Compare(a.Name, b.Name)
})

for _, c := range cs {
if !f(c) {
break
}
}
}

// CloseUpstreams closes upstream configurations of persistent clients.
func (ci *Index) CloseUpstreams() (err error) {
var errs []error
ci.RangeByName(func(c *Persistent) (cont bool) {
err = c.CloseUpstreams()
if err != nil {
errs = append(errs, err)
}

return true
})

return errors.Join(errs...)
}
58 changes: 55 additions & 3 deletions internal/client/index_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func newIDIndex(m []*Persistent) (ci *Index) {
return ci
}

func TestClientIndex(t *testing.T) {
func TestClientIndex_Find(t *testing.T) {
const (
cliIPNone = "1.2.3.4"
cliIP1 = "1.1.1.1"
Expand Down Expand Up @@ -71,13 +71,14 @@ func TestClientIndex(t *testing.T) {
}
)

ci := newIDIndex([]*Persistent{
clients := []*Persistent{
clientWithBothFams,
clientWithSubnet,
clientWithMAC,
clientWithID,
clientLinkLocal,
})
}
ci := newIDIndex(clients)

testCases := []struct {
want *Persistent
Expand Down Expand Up @@ -296,3 +297,54 @@ func TestIndex_FindByIPWithoutZone(t *testing.T) {
})
}
}

func TestClientIndex_RangeByName(t *testing.T) {
sortedClients := []*Persistent{{
Name: "clientA",
ClientIDs: []string{"A"},
}, {
Name: "clientB",
ClientIDs: []string{"B"},
}, {
Name: "clientC",
ClientIDs: []string{"C"},
}, {
Name: "clientD",
ClientIDs: []string{"D"},
}, {
Name: "clientE",
ClientIDs: []string{"E"},
}}

testCases := []struct {
name string
want []*Persistent
}{{
name: "basic",
want: sortedClients,
}, {
name: "nil",
want: nil,
}, {
name: "one_element",
want: sortedClients[:1],
}, {
name: "two_elements",
want: sortedClients[:2],
}}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ci := newIDIndex(tc.want)

var got []*Persistent
ci.RangeByName(func(c *Persistent) (cont bool) {
got = append(got, c)

return true
})

assert.Equal(t, tc.want, got)
})
}
}

0 comments on commit 71c44fa

Please sign in to comment.