control/controlclient: remove quadratic allocs in mapSession

The mapSession code was previously quadratic: N clients in a netmap
send updates proportional to N and then for each, we do N units of
work. This removes most of that "N units of work" per update. There's
still a netmap-sized slice allocation per update (that's #8963), but
that's it.

Bit more efficient now, especially with larger netmaps:

                                 │     before     │                after                │
                                 │     sec/op     │   sec/op     vs base                │
    MapSessionDelta/size_10-8       47.935µ ±  3%   1.232µ ± 2%  -97.43% (p=0.000 n=10)
    MapSessionDelta/size_100-8      79.950µ ±  3%   1.642µ ± 2%  -97.95% (p=0.000 n=10)
    MapSessionDelta/size_1000-8    355.747µ ± 10%   4.400µ ± 1%  -98.76% (p=0.000 n=10)
    MapSessionDelta/size_10000-8   3079.71µ ±  3%   27.89µ ± 3%  -99.09% (p=0.000 n=10)
    geomean                          254.6µ         3.969µ       -98.44%

                                 │     before     │                after                 │
                                 │      B/op      │     B/op      vs base                │
    MapSessionDelta/size_10-8        9.651Ki ± 0%   2.395Ki ± 0%  -75.19% (p=0.000 n=10)
    MapSessionDelta/size_100-8      83.097Ki ± 0%   3.192Ki ± 0%  -96.16% (p=0.000 n=10)
    MapSessionDelta/size_1000-8     800.25Ki ± 0%   10.32Ki ± 0%  -98.71% (p=0.000 n=10)
    MapSessionDelta/size_10000-8   7896.04Ki ± 0%   82.32Ki ± 0%  -98.96% (p=0.000 n=10)
    geomean                          266.8Ki        8.977Ki       -96.64%

                                 │    before     │               after                │
                                 │   allocs/op   │ allocs/op   vs base                │
    MapSessionDelta/size_10-8         72.00 ± 0%   20.00 ± 0%  -72.22% (p=0.000 n=10)
    MapSessionDelta/size_100-8       523.00 ± 0%   20.00 ± 0%  -96.18% (p=0.000 n=10)
    MapSessionDelta/size_1000-8     5024.00 ± 0%   20.00 ± 0%  -99.60% (p=0.000 n=10)
    MapSessionDelta/size_10000-8   50024.00 ± 0%   20.00 ± 0%  -99.96% (p=0.000 n=10)
    geomean                          1.754k        20.00       -98.86%

Updates #1909

Change-Id: I41ee29358a5521ed762216a76d4cc5b0d16e46ac
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/9013/head
Brad Fitzpatrick 1 year ago committed by Brad Fitzpatrick
parent a3b0654ed8
commit db017d3b12

@ -6,7 +6,6 @@ package controlclient
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net/netip" "net/netip"
"sort" "sort"
@ -16,6 +15,7 @@ import (
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/ptr"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/cmpx" "tailscale.com/util/cmpx"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
@ -33,6 +33,7 @@ type mapSession struct {
// Immutable fields. // Immutable fields.
nu NetmapUpdater // called on changes (in addition to the optional hooks below) nu NetmapUpdater // called on changes (in addition to the optional hooks below)
privateNodeKey key.NodePrivate privateNodeKey key.NodePrivate
publicNodeKey key.NodePublic
logf logger.Logf logf logger.Logf
vlogf logger.Logf vlogf logger.Logf
machinePubKey key.MachinePublic machinePubKey key.MachinePublic
@ -63,6 +64,8 @@ type mapSession struct {
// Fields storing state over the course of multiple MapResponses. // Fields storing state over the course of multiple MapResponses.
lastNode tailcfg.NodeView lastNode tailcfg.NodeView
peers map[tailcfg.NodeID]*tailcfg.NodeView // pointer to view (oddly). same pointers as sortedPeers.
sortedPeers []*tailcfg.NodeView // same pointers as peers, but sorted by Node.ID
lastDNSConfig *tailcfg.DNSConfig lastDNSConfig *tailcfg.DNSConfig
lastDERPMap *tailcfg.DERPMap lastDERPMap *tailcfg.DERPMap
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
@ -70,7 +73,6 @@ type mapSession struct {
lastParsedPacketFilter []filter.Match lastParsedPacketFilter []filter.Match
lastSSHPolicy *tailcfg.SSHPolicy lastSSHPolicy *tailcfg.SSHPolicy
collectServices bool collectServices bool
previousPeers []*tailcfg.Node // for delta-purposes
lastDomain string lastDomain string
lastDomainAuditLogID string lastDomainAuditLogID string
lastHealth []string lastHealth []string
@ -78,10 +80,6 @@ type mapSession struct {
stickyDebug tailcfg.Debug // accumulated opt.Bool values stickyDebug tailcfg.Debug // accumulated opt.Bool values
lastTKAInfo *tailcfg.TKAInfo lastTKAInfo *tailcfg.TKAInfo
lastNetmapSummary string // from NetworkMap.VeryConcise lastNetmapSummary string // from NetworkMap.VeryConcise
// netMapBuilding is non-nil during a netmapForResponse call,
// containing the value to be returned, once fully populated.
netMapBuilding *netmap.NetworkMap
} }
// newMapSession returns a mostly unconfigured new mapSession. // newMapSession returns a mostly unconfigured new mapSession.
@ -93,6 +91,7 @@ func newMapSession(privateNodeKey key.NodePrivate, nu NetmapUpdater) *mapSession
ms := &mapSession{ ms := &mapSession{
nu: nu, nu: nu,
privateNodeKey: privateNodeKey, privateNodeKey: privateNodeKey,
publicNodeKey: privateNodeKey.Public(),
lastDNSConfig: new(tailcfg.DNSConfig), lastDNSConfig: new(tailcfg.DNSConfig),
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{}, lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
watchdogReset: make(chan struct{}), watchdogReset: make(chan struct{}),
@ -184,7 +183,9 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
// Call Node.InitDisplayNames on any changed nodes. // Call Node.InitDisplayNames on any changed nodes.
initDisplayNames(cmpx.Or(resp.Node.View(), ms.lastNode), resp) initDisplayNames(cmpx.Or(resp.Node.View(), ms.lastNode), resp)
nm := ms.netmapForResponse(resp) ms.updateStateFromResponse(resp)
nm := ms.netmap()
ms.lastNetmapSummary = nm.VeryConcise() ms.lastNetmapSummary = nm.VeryConcise()
ms.onConciseNetMapSummary(ms.lastNetmapSummary) ms.onConciseNetMapSummary(ms.lastNetmapSummary)
@ -198,30 +199,28 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
return nil return nil
} }
func (ms *mapSession) addUserProfile(userID tailcfg.UserID) { // updateStats are some stats from updateStateFromResponse, primarily for
if userID == 0 { // testing. It's meant to be cheap enough to always compute, though. It doesn't
return // allocate.
} type updateStats struct {
nm := ms.netMapBuilding allNew bool
if _, dup := nm.UserProfiles[userID]; dup { added int
// Already populated it from a previous peer. removed int
return changed int
}
if up, ok := ms.lastUserProfile[userID]; ok {
nm.UserProfiles[userID] = up
}
} }
// netmapForResponse returns a fully populated NetworkMap from a full // updateStateFromResponse updates ms from res. It takes ownership of res.
// or incremental MapResponse within the session, filling in omitted func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
// information from prior MapResponse values. ms.updatePeersStateFromResponse(resp)
func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.NetworkMap {
undeltaPeers(resp, ms.previousPeers) if resp.Node != nil {
ms.lastNode = resp.Node.View()
}
ms.previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
for _, up := range resp.UserProfiles { for _, up := range resp.UserProfiles {
ms.lastUserProfile[up.ID] = up ms.lastUserProfile[up.ID] = up
} }
// TODO(bradfitz): clean up old user profiles? maybe not worth it.
if dm := resp.DERPMap; dm != nil { if dm := resp.DERPMap; dm != nil {
ms.vlogf("netmap: new map contains DERP map") ms.vlogf("netmap: new map contains DERP map")
@ -277,206 +276,216 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
if resp.TKAInfo != nil { if resp.TKAInfo != nil {
ms.lastTKAInfo = resp.TKAInfo ms.lastTKAInfo = resp.TKAInfo
} }
}
// TODO(bradfitz): now that this is a view, remove some of the defensive // updatePeersStateFromResponseres updates ms.peers and ms.sortedPeers from res. It takes ownership of res.
// cloning elsewhere in mapSession. func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (stats updateStats) {
peerViews := make([]tailcfg.NodeView, len(resp.Peers)) defer func() {
for i, n := range resp.Peers { if stats.removed > 0 || stats.added > 0 {
peerViews[i] = n.View() ms.rebuildSorted()
} }
}()
nm := &netmap.NetworkMap{ if ms.peers == nil {
NodeKey: ms.privateNodeKey.Public(), ms.peers = make(map[tailcfg.NodeID]*tailcfg.NodeView)
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: peerViews,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
PacketFilterRules: ms.lastPacketFilterRules,
SSHPolicy: ms.lastSSHPolicy,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
} }
ms.netMapBuilding = nm
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" { if len(resp.Peers) > 0 {
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil { // Not delta encoded.
ms.logf("error unmarshalling TKAHead: %v", err) stats.allNew = true
nm.TKAEnabled = false keep := make(map[tailcfg.NodeID]bool, len(resp.Peers))
} for _, n := range resp.Peers {
keep[n.ID] = true
if vp, ok := ms.peers[n.ID]; ok {
stats.changed++
*vp = n.View()
} else {
stats.added++
ms.peers[n.ID] = ptr.To(n.View())
} }
if resp.Node != nil {
ms.lastNode = resp.Node.View()
} }
if node := ms.lastNode; node.Valid() { for id := range ms.peers {
nm.SelfNode = node if !keep[id] {
nm.Expiry = node.KeyExpiry() stats.removed++
nm.Name = node.Name() delete(ms.peers, id)
nm.Addresses = filterSelfAddresses(node.Addresses().AsSlice())
if node.Hostinfo().Valid() {
nm.Hostinfo = *node.Hostinfo().AsStruct()
} }
if node.MachineAuthorized() {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
} }
// Peers precludes all other delta operations so just return.
return
} }
ms.addUserProfile(nm.User()) for _, id := range resp.PeersRemoved {
for _, peer := range resp.Peers { if _, ok := ms.peers[id]; ok {
ms.addUserProfile(peer.Sharer) delete(ms.peers, id)
ms.addUserProfile(peer.User) stats.removed++
} }
if DevKnob.ForceProxyDNS() {
nm.DNS.Proxied = true
} }
ms.netMapBuilding = nil
return nm
}
// undeltaPeers updates mapRes.Peers to be complete based on the for _, n := range resp.PeersChanged {
// provided previous peer list and the PeersRemoved and PeersChanged if vp, ok := ms.peers[n.ID]; ok {
// fields in mapRes, as well as the PeerSeenChange and OnlineChange stats.changed++
// maps. *vp = n.View()
// } else {
// It then also nils out the delta fields. stats.added++
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) { ms.peers[n.ID] = ptr.To(n.View())
if len(mapRes.Peers) > 0 {
// Not delta encoded.
if !nodesSorted(mapRes.Peers) {
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
sortNodes(mapRes.Peers)
} }
return
} }
var removed map[tailcfg.NodeID]bool for nodeID, seen := range resp.PeerSeenChange {
if pr := mapRes.PeersRemoved; len(pr) > 0 { if vp, ok := ms.peers[nodeID]; ok {
removed = make(map[tailcfg.NodeID]bool, len(pr)) mut := vp.AsStruct()
for _, id := range pr { if seen {
removed[id] = true mut.LastSeen = ptr.To(clock.Now())
} else {
mut.LastSeen = nil
}
*vp = mut.View()
stats.changed++
} }
} }
changed := mapRes.PeersChanged
if !nodesSorted(changed) { for nodeID, online := range resp.OnlineChange {
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting") if vp, ok := ms.peers[nodeID]; ok {
sortNodes(changed) mut := vp.AsStruct()
mut.Online = ptr.To(online)
*vp = mut.View()
stats.changed++
} }
if !nodesSorted(prev) {
// Internal error (unrelated to the network) if we get here.
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
sortNodes(prev)
} }
newFull := prev for _, pc := range resp.PeersChangedPatch {
if len(removed) > 0 || len(changed) > 0 { vp, ok := ms.peers[pc.NodeID]
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed)) if !ok {
for len(prev) > 0 && len(changed) > 0 {
pID := prev[0].ID
cID := changed[0].ID
if removed[pID] {
prev = prev[1:]
continue continue
} }
switch { stats.changed++
case pID < cID: mut := vp.AsStruct()
newFull = append(newFull, prev[0]) if pc.DERPRegion != 0 {
prev = prev[1:] mut.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, pc.DERPRegion)
case pID == cID:
newFull = append(newFull, changed[0])
prev, changed = prev[1:], changed[1:]
case cID < pID:
newFull = append(newFull, changed[0])
changed = changed[1:]
} }
if pc.Cap != 0 {
mut.Cap = pc.Cap
} }
newFull = append(newFull, changed...) if pc.Endpoints != nil {
for _, n := range prev { mut.Endpoints = pc.Endpoints
if !removed[n.ID] {
newFull = append(newFull, n)
} }
if pc.Key != nil {
mut.Key = *pc.Key
} }
sortNodes(newFull) if pc.DiscoKey != nil {
mut.DiscoKey = *pc.DiscoKey
} }
if v := pc.Online; v != nil {
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 || len(mapRes.PeersChangedPatch) != 0 { mut.Online = ptr.To(*v)
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
} }
now := clock.Now() if v := pc.LastSeen; v != nil {
for nodeID, seen := range mapRes.PeerSeenChange { mut.LastSeen = ptr.To(*v)
if n, ok := peerByID[nodeID]; ok {
if seen {
n.LastSeen = &now
} else {
n.LastSeen = nil
} }
if v := pc.KeyExpiry; v != nil {
mut.KeyExpiry = *v
} }
if v := pc.Capabilities; v != nil {
mut.Capabilities = *v
} }
for nodeID, online := range mapRes.OnlineChange { if v := pc.KeySignature; v != nil {
if n, ok := peerByID[nodeID]; ok { mut.KeySignature = v
online := online
n.Online = &online
} }
*vp = mut.View()
} }
for _, ec := range mapRes.PeersChangedPatch {
if n, ok := peerByID[ec.NodeID]; ok { return
if ec.DERPRegion != 0 { }
n.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, ec.DERPRegion)
// rebuildSorted rebuilds ms.sortedPeers from ms.peers. It should be called
// after any additions or removals from peers.
func (ms *mapSession) rebuildSorted() {
if ms.sortedPeers == nil {
ms.sortedPeers = make([]*tailcfg.NodeView, 0, len(ms.peers))
} else {
if len(ms.sortedPeers) > len(ms.peers) {
clear(ms.sortedPeers[len(ms.peers):])
} }
if ec.Cap != 0 { ms.sortedPeers = ms.sortedPeers[:0]
n.Cap = ec.Cap
} }
if ec.Endpoints != nil { for _, p := range ms.peers {
n.Endpoints = ec.Endpoints ms.sortedPeers = append(ms.sortedPeers, p)
} }
if ec.Key != nil { sort.Slice(ms.sortedPeers, func(i, j int) bool {
n.Key = *ec.Key return ms.sortedPeers[i].ID() < ms.sortedPeers[j].ID()
})
}
func (ms *mapSession) addUserProfile(nm *netmap.NetworkMap, userID tailcfg.UserID) {
if userID == 0 {
return
} }
if ec.DiscoKey != nil { if _, dup := nm.UserProfiles[userID]; dup {
n.DiscoKey = *ec.DiscoKey // Already populated it from a previous peer.
return
} }
if v := ec.Online; v != nil { if up, ok := ms.lastUserProfile[userID]; ok {
n.Online = ptrCopy(v) nm.UserProfiles[userID] = up
} }
if v := ec.LastSeen; v != nil { }
n.LastSeen = ptrCopy(v)
// netmap returns a fully populated NetworkMap from the last state seen from
// a call to updateStateFromResponse, filling in omitted
// information from prior MapResponse values.
func (ms *mapSession) netmap() *netmap.NetworkMap {
peerViews := make([]tailcfg.NodeView, len(ms.sortedPeers))
for i, vp := range ms.sortedPeers {
peerViews[i] = *vp
} }
if v := ec.KeyExpiry; v != nil {
n.KeyExpiry = *v nm := &netmap.NetworkMap{
NodeKey: ms.publicNodeKey,
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: peerViews,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
PacketFilterRules: ms.lastPacketFilterRules,
SSHPolicy: ms.lastSSHPolicy,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
} }
if v := ec.Capabilities; v != nil {
n.Capabilities = *v if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
ms.logf("error unmarshalling TKAHead: %v", err)
nm.TKAEnabled = false
} }
if v := ec.KeySignature; v != nil {
n.KeySignature = v
} }
if node := ms.lastNode; node.Valid() {
nm.SelfNode = node
nm.Expiry = node.KeyExpiry()
nm.Name = node.Name()
nm.Addresses = filterSelfAddresses(node.Addresses().AsSlice())
if node.Hostinfo().Valid() {
nm.Hostinfo = *node.Hostinfo().AsStruct()
} }
if node.MachineAuthorized() {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
} }
} }
mapRes.Peers = newFull ms.addUserProfile(nm, nm.User())
mapRes.PeersChanged = nil for _, peer := range peerViews {
mapRes.PeersRemoved = nil ms.addUserProfile(nm, peer.Sharer())
} ms.addUserProfile(nm, peer.User())
// ptrCopy returns a pointer to a newly allocated shallow copy of *v.
func ptrCopy[T any](v *T) *T {
if v == nil {
return nil
} }
ret := new(T) if DevKnob.ForceProxyDNS() {
*ret = *v nm.DNS.Proxied = true
return ret }
return nm
} }
func nodesSorted(v []*tailcfg.Node) bool { func nodesSorted(v []*tailcfg.Node) bool {

@ -21,10 +21,11 @@ import (
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/util/mak"
"tailscale.com/util/must" "tailscale.com/util/must"
) )
func TestUndeltaPeers(t *testing.T) { func TestUpdatePeersStateFromResponse(t *testing.T) {
var curTime time.Time var curTime time.Time
online := func(v bool) func(*tailcfg.Node) { online := func(v bool) func(*tailcfg.Node) {
@ -61,6 +62,7 @@ func TestUndeltaPeers(t *testing.T) {
curTime time.Time curTime time.Time
prev []*tailcfg.Node prev []*tailcfg.Node
want []*tailcfg.Node want []*tailcfg.Node
wantStats updateStats
}{ }{
{ {
name: "full_peers", name: "full_peers",
@ -68,6 +70,10 @@ func TestUndeltaPeers(t *testing.T) {
Peers: peers(n(1, "foo"), n(2, "bar")), Peers: peers(n(1, "foo"), n(2, "bar")),
}, },
want: peers(n(1, "foo"), n(2, "bar")), want: peers(n(1, "foo"), n(2, "bar")),
wantStats: updateStats{
allNew: true,
added: 2,
},
}, },
{ {
name: "full_peers_ignores_deltas", name: "full_peers_ignores_deltas",
@ -76,6 +82,10 @@ func TestUndeltaPeers(t *testing.T) {
PeersRemoved: []tailcfg.NodeID{2}, PeersRemoved: []tailcfg.NodeID{2},
}, },
want: peers(n(1, "foo"), n(2, "bar")), want: peers(n(1, "foo"), n(2, "bar")),
wantStats: updateStats{
allNew: true,
added: 2,
},
}, },
{ {
name: "add_and_update", name: "add_and_update",
@ -84,14 +94,21 @@ func TestUndeltaPeers(t *testing.T) {
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")), PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
}, },
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")), want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
wantStats: updateStats{
added: 2, // added IDs 0 and 3
changed: 1, // changed ID 2
},
}, },
{ {
name: "remove", name: "remove",
prev: peers(n(1, "foo"), n(2, "bar")), prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
PeersRemoved: []tailcfg.NodeID{1}, PeersRemoved: []tailcfg.NodeID{1, 3, 4},
}, },
want: peers(n(2, "bar")), want: peers(n(2, "bar")),
wantStats: updateStats{
removed: 1, // ID 1
},
}, },
{ {
name: "add_and_remove", name: "add_and_remove",
@ -101,6 +118,10 @@ func TestUndeltaPeers(t *testing.T) {
PeersRemoved: []tailcfg.NodeID{2}, PeersRemoved: []tailcfg.NodeID{2},
}, },
want: peers(n(1, "foo2")), want: peers(n(1, "foo2")),
wantStats: updateStats{
changed: 1,
removed: 1,
},
}, },
{ {
name: "unchanged", name: "unchanged",
@ -114,12 +135,14 @@ func TestUndeltaPeers(t *testing.T) {
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
OnlineChange: map[tailcfg.NodeID]bool{ OnlineChange: map[tailcfg.NodeID]bool{
1: true, 1: true,
404: true,
}, },
}, },
want: peers( want: peers(
n(1, "foo", online(true)), n(1, "foo", online(true)),
n(2, "bar"), n(2, "bar"),
), ),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "online_change_offline", name: "online_change_offline",
@ -134,6 +157,7 @@ func TestUndeltaPeers(t *testing.T) {
n(1, "foo", online(false)), n(1, "foo", online(false)),
n(2, "bar", online(true)), n(2, "bar", online(true)),
), ),
wantStats: updateStats{changed: 2},
}, },
{ {
name: "peer_seen_at", name: "peer_seen_at",
@ -149,6 +173,7 @@ func TestUndeltaPeers(t *testing.T) {
n(1, "foo"), n(1, "foo"),
n(2, "bar", seenAt(time.Unix(123, 0))), n(2, "bar", seenAt(time.Unix(123, 0))),
), ),
wantStats: updateStats{changed: 2},
}, },
{ {
name: "ep_change_derp", name: "ep_change_derp",
@ -160,6 +185,7 @@ func TestUndeltaPeers(t *testing.T) {
}}, }},
}, },
want: peers(n(1, "foo", withDERP("127.3.3.40:4"))), want: peers(n(1, "foo", withDERP("127.3.3.40:4"))),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "ep_change_udp", name: "ep_change_udp",
@ -171,9 +197,10 @@ func TestUndeltaPeers(t *testing.T) {
}}, }},
}, },
want: peers(n(1, "foo", withEP("1.2.3.4:56"))), want: peers(n(1, "foo", withEP("1.2.3.4:56"))),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "ep_change_udp", name: "ep_change_udp_2",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))), prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))),
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{ PeersChangedPatch: []*tailcfg.PeerChange{{
@ -182,6 +209,7 @@ func TestUndeltaPeers(t *testing.T) {
}}, }},
}, },
want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))), want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "ep_change_both", name: "ep_change_both",
@ -194,6 +222,7 @@ func TestUndeltaPeers(t *testing.T) {
}}, }},
}, },
want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))), want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_key", name: "change_key",
@ -208,6 +237,7 @@ func TestUndeltaPeers(t *testing.T) {
Name: "foo", Name: "foo",
Key: key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))), Key: key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))),
}), }),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_key_signature", name: "change_key_signature",
@ -217,11 +247,13 @@ func TestUndeltaPeers(t *testing.T) {
NodeID: 1, NodeID: 1,
KeySignature: []byte{3, 4}, KeySignature: []byte{3, 4},
}}, }},
}, want: peers(&tailcfg.Node{ },
want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
KeySignature: []byte{3, 4}, KeySignature: []byte{3, 4},
}), }),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_disco_key", name: "change_disco_key",
@ -231,11 +263,13 @@ func TestUndeltaPeers(t *testing.T) {
NodeID: 1, NodeID: 1,
DiscoKey: ptr.To(key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))), DiscoKey: ptr.To(key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
}}, }},
}, want: peers(&tailcfg.Node{ },
want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
DiscoKey: key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))), DiscoKey: key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))),
}), }),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_online", name: "change_online",
@ -245,11 +279,13 @@ func TestUndeltaPeers(t *testing.T) {
NodeID: 1, NodeID: 1,
Online: ptr.To(true), Online: ptr.To(true),
}}, }},
}, want: peers(&tailcfg.Node{ },
want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
Online: ptr.To(true), Online: ptr.To(true),
}), }),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_last_seen", name: "change_last_seen",
@ -259,11 +295,13 @@ func TestUndeltaPeers(t *testing.T) {
NodeID: 1, NodeID: 1,
LastSeen: ptr.To(time.Unix(123, 0).UTC()), LastSeen: ptr.To(time.Unix(123, 0).UTC()),
}}, }},
}, want: peers(&tailcfg.Node{ },
want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
LastSeen: ptr.To(time.Unix(123, 0).UTC()), LastSeen: ptr.To(time.Unix(123, 0).UTC()),
}), }),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_key_expiry", name: "change_key_expiry",
@ -273,11 +311,13 @@ func TestUndeltaPeers(t *testing.T) {
NodeID: 1, NodeID: 1,
KeyExpiry: ptr.To(time.Unix(123, 0).UTC()), KeyExpiry: ptr.To(time.Unix(123, 0).UTC()),
}}, }},
}, want: peers(&tailcfg.Node{ },
want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
KeyExpiry: time.Unix(123, 0).UTC(), KeyExpiry: time.Unix(123, 0).UTC(),
}), }),
wantStats: updateStats{changed: 1},
}, },
{ {
name: "change_capabilities", name: "change_capabilities",
@ -287,11 +327,13 @@ func TestUndeltaPeers(t *testing.T) {
NodeID: 1, NodeID: 1,
Capabilities: ptr.To([]string{"foo"}), Capabilities: ptr.To([]string{"foo"}),
}}, }},
}, want: peers(&tailcfg.Node{ },
want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
Capabilities: []string{"foo"}, Capabilities: []string{"foo"},
}), }),
wantStats: updateStats{changed: 1},
}} }}
for _, tt := range tests { for _, tt := range tests {
@ -300,9 +342,23 @@ func TestUndeltaPeers(t *testing.T) {
curTime = tt.curTime curTime = tt.curTime
tstest.Replace(t, &clock, tstime.Clock(tstest.NewClock(tstest.ClockOpts{Start: curTime}))) tstest.Replace(t, &clock, tstime.Clock(tstest.NewClock(tstest.ClockOpts{Start: curTime})))
} }
undeltaPeers(tt.mapRes, tt.prev) ms := newTestMapSession(t, nil)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) { for _, n := range tt.prev {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want)) mak.Set(&ms.peers, n.ID, ptr.To(n.View()))
}
ms.rebuildSorted()
gotStats := ms.updatePeersStateFromResponse(tt.mapRes)
got := make([]*tailcfg.Node, len(ms.sortedPeers))
for i, vp := range ms.sortedPeers {
got[i] = vp.AsStruct()
}
if gotStats != tt.wantStats {
t.Errorf("got stats = %+v; want %+v", gotStats, tt.wantStats)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(got), formatNodes(tt.want))
} }
}) })
} }
@ -340,6 +396,11 @@ func newTestMapSession(t testing.TB, nu NetmapUpdater) *mapSession {
return ms return ms
} }
func (ms *mapSession) netmapForResponse(res *tailcfg.MapResponse) *netmap.NetworkMap {
ms.updateStateFromResponse(res)
return ms.netmap()
}
func TestNetmapForResponse(t *testing.T) { func TestNetmapForResponse(t *testing.T) {
t.Run("implicit_packetfilter", func(t *testing.T) { t.Run("implicit_packetfilter", func(t *testing.T) {
somePacketFilter := []tailcfg.FilterRule{ somePacketFilter := []tailcfg.FilterRule{
@ -454,7 +515,8 @@ func TestNetmapForResponse(t *testing.T) {
Node: someNode, Node: someNode,
} }
initDisplayNames(mapRes.Node.View(), mapRes) initDisplayNames(mapRes.Node.View(), mapRes)
nm1 := ms.netmapForResponse(mapRes) ms.updateStateFromResponse(mapRes)
nm1 := ms.netmap()
if !nm1.SelfNode.Valid() { if !nm1.SelfNode.Valid() {
t.Fatal("nil Node in 1st netmap") t.Fatal("nil Node in 1st netmap")
} }
@ -463,7 +525,8 @@ func TestNetmapForResponse(t *testing.T) {
t.Errorf("Node mismatch in 1st netmap; got: %s", j) t.Errorf("Node mismatch in 1st netmap; got: %s", j)
} }
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{}) ms.updateStateFromResponse(&tailcfg.MapResponse{})
nm2 := ms.netmap()
if !nm2.SelfNode.Valid() { if !nm2.SelfNode.Valid() {
t.Fatal("nil Node in 1st netmap") t.Fatal("nil Node in 1st netmap")
} }

Loading…
Cancel
Save