mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-15 07:20:36 -04:00
* ipn/ipnlocal: fix cache update for peers deleted by netmap deltas (#20851) After a netmap delta is applied, we scan the mutations for affected peers and update the cache (if enabled) for those peers. For removals in particular, we were relying on the node backend to resolve node IDs (provided by the delta mutation) to stable IDs. Prior to65fd320athis happened to work because the node backend would hold on to all the peers mentioned by the previous full netmap, even after applying deltas. But that was essentially accidental, and once we fixed it not to do that, these lookups no longer worked. We need the stable ID, since that is how the cache is keyed, and now that they're no longer pinned, we were not properly evicting removed peers from the cache. To fix this, capture removed peer stable IDs while applying mutations to the node backend, instead of trying to look them up afterward. Updates #20796 Change-Id: I14ded78eaf9657645f0869a52460fd3cd86edba6 Signed-off-by: M. J. Fromberger <fromberger@tailscale.com> (cherry picked from commitf3552c29c0) * ipn/ipnlocal: don't evict another node's index entries on netmap deltas When applying netmap deltas, nodeBackend evicted its index entries (nodeByAddr, nodeByKey, nodeByWGString, nodeByStableID, nodeByName) derived from a node's last-known value without checking that the entry still pointed at that node. Control can reassign a churning ephemeral peer's Tailscale IP (or MagicDNS name) to a newer peer and deliver the new peer's upsert before the old peer's removal, either in an earlier MapResponse or reordered within one batch by the NodeID sort in netmap.MutationsFromMapResponse. The removal then wiped the new owner's entry. The peers map itself stayed correct in every ordering, so WireGuard kept the peer and handshakes succeeded, but WhoIs lookups by IP failed until the next full netmap rebuilt the indexes. On App Connectors that surfaced as "peerapi: unknown peer" and refused DNS connections from affected clients, with a toggle of Tailscale (forcing a full netmap) as the only recovery. Make every index eviction conditional on the entry still mapping to the node being removed or replaced, and add a regression test covering the cross-batch, intra-batch, and upsert-eviction orderings. Also add an end-to-end test in tstest/integration showing that a MapResponse reusing an address is handled incrementally rather than as a full netmap, and that LocalBackend.WhoIs still resolves the reused address afterwards, which is the lookup PeerAPI makes before it accepts a connection. Updates tailscale/corp#47435 Co-authored-by: Brendan Creane <bcreane@gmail.com> Signed-off-by: Brendan Creane <bcreane@gmail.com> Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I3f8c2a9d41e07b6a5cd2e94f78b013c6ad2f5e91 (cherry picked from commit2ae2808b64) * ipn/ipnlocal: preserve peer deltas on expiry Refresh the expiry timer netmap from the live peer state before reinstalling it, preventing delta updates from being rolled back. Updates tailscale/corp#47686 Change-Id: Idc738acea82bab5a8ba772084a41e55b38a06bcc Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> (cherry picked from commit0640312e51) * control/controlclient: replay user profiles on delta peer upserts A full netmap carries only the profiles of users with a currently visible peer (netmapForResponse), and nodeBackend replaces its live profile set wholesale on every full netmap install. A full netmap that arrives while a user has no visible peer therefore drops that user's profile downstream. When a peer of that user later returns as an incremental upsert, control does not resend the profile, because MapResponse.UserProfiles has carried only new or updated profiles since mapver 5. The upsert indexes the node by address and key, so WireGuard admits its traffic, but WhoIs then fails one step later at the user profile lookup, surfacing as "peerapi: unknown peer" until the next full netmap. It is a second, independent cause of the symptom fixed by the recent index eviction change. mapSession.lastUserProfile holds the profile the whole time, so when handling a response incrementally, also deliver the profiles of upserted peers' users (and sharers) from that store, before the mutations that reference them. The backport adapts the new tests to this branch's extra viaTSMP parameter on handleNonKeepAliveMapResponse. Updates tailscale/corp#47435 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I9c4b2f6a8e0d47d3b1a5c2e7f4098d61b23a7c50 (cherry picked from commit5201273aec) --------- Signed-off-by: M. J. Fromberger <fromberger@tailscale.com> Signed-off-by: Brendan Creane <bcreane@gmail.com> Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Co-authored-by: M. J. Fromberger <fromberger@tailscale.com> Co-authored-by: Brendan Creane <bcreane@gmail.com>
2233 lines
61 KiB
Go
2233 lines
61 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package controlclient
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"net/netip"
|
|
"reflect"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"go4.org/mem"
|
|
"tailscale.com/control/controlknobs"
|
|
"tailscale.com/health"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/tstime"
|
|
"tailscale.com/types/dnstype"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/netmap"
|
|
"tailscale.com/types/persist"
|
|
"tailscale.com/util/eventbus/eventbustest"
|
|
"tailscale.com/util/mak"
|
|
"tailscale.com/util/must"
|
|
"tailscale.com/util/zstdframe"
|
|
)
|
|
|
|
func eps(s ...string) []netip.AddrPort {
|
|
var eps []netip.AddrPort
|
|
for _, ep := range s {
|
|
eps = append(eps, netip.MustParseAddrPort(ep))
|
|
}
|
|
return eps
|
|
}
|
|
|
|
func TestUpdatePeersStateFromResponse(t *testing.T) {
|
|
var curTime time.Time
|
|
|
|
online := func(v bool) func(*tailcfg.Node) {
|
|
return func(n *tailcfg.Node) {
|
|
n.Online = &v
|
|
}
|
|
}
|
|
seenAt := func(t time.Time) func(*tailcfg.Node) {
|
|
return func(n *tailcfg.Node) {
|
|
n.LastSeen = &t
|
|
}
|
|
}
|
|
withDERP := func(regionID int) func(*tailcfg.Node) {
|
|
return func(n *tailcfg.Node) {
|
|
n.HomeDERP = regionID
|
|
}
|
|
}
|
|
withEP := func(ep string) func(*tailcfg.Node) {
|
|
return func(n *tailcfg.Node) {
|
|
n.Endpoints = []netip.AddrPort{netip.MustParseAddrPort(ep)}
|
|
}
|
|
}
|
|
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
|
n := &tailcfg.Node{ID: id, Name: name}
|
|
for _, f := range mod {
|
|
f(n)
|
|
}
|
|
return n
|
|
}
|
|
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
|
tests := []struct {
|
|
name string
|
|
mapRes *tailcfg.MapResponse
|
|
curTime time.Time
|
|
prev []*tailcfg.Node
|
|
want []*tailcfg.Node
|
|
wantStats updateStats
|
|
}{
|
|
{
|
|
name: "full_peers",
|
|
mapRes: &tailcfg.MapResponse{
|
|
Peers: 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",
|
|
mapRes: &tailcfg.MapResponse{
|
|
Peers: peers(n(1, "foo"), n(2, "bar")),
|
|
PeersRemoved: []tailcfg.NodeID{2},
|
|
},
|
|
want: peers(n(1, "foo"), n(2, "bar")),
|
|
wantStats: updateStats{
|
|
allNew: true,
|
|
added: 2,
|
|
},
|
|
},
|
|
{
|
|
name: "add_and_update",
|
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
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")),
|
|
wantStats: updateStats{
|
|
added: 2, // added IDs 0 and 3
|
|
changed: 1, // changed ID 2
|
|
},
|
|
},
|
|
{
|
|
name: "remove",
|
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersRemoved: []tailcfg.NodeID{1, 3, 4},
|
|
},
|
|
want: peers(n(2, "bar")),
|
|
wantStats: updateStats{
|
|
removed: 1, // ID 1
|
|
},
|
|
},
|
|
{
|
|
name: "add_and_remove",
|
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChanged: peers(n(1, "foo2")),
|
|
PeersRemoved: []tailcfg.NodeID{2},
|
|
},
|
|
want: peers(n(1, "foo2")),
|
|
wantStats: updateStats{
|
|
changed: 1,
|
|
removed: 1,
|
|
},
|
|
},
|
|
{
|
|
name: "unchanged",
|
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
|
mapRes: &tailcfg.MapResponse{},
|
|
want: peers(n(1, "foo"), n(2, "bar")),
|
|
},
|
|
{
|
|
name: "online_change",
|
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
OnlineChange: map[tailcfg.NodeID]bool{
|
|
1: true,
|
|
404: true,
|
|
},
|
|
},
|
|
want: peers(
|
|
n(1, "foo", online(true)),
|
|
n(2, "bar"),
|
|
),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "online_change_offline",
|
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
OnlineChange: map[tailcfg.NodeID]bool{
|
|
1: false,
|
|
2: true,
|
|
},
|
|
},
|
|
want: peers(
|
|
n(1, "foo", online(false)),
|
|
n(2, "bar", online(true)),
|
|
),
|
|
wantStats: updateStats{changed: 2},
|
|
},
|
|
{
|
|
name: "peer_seen_at",
|
|
prev: peers(n(1, "foo", seenAt(time.Unix(111, 0))), n(2, "bar")),
|
|
curTime: time.Unix(123, 0),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeerSeenChange: map[tailcfg.NodeID]bool{
|
|
1: false,
|
|
2: true,
|
|
},
|
|
},
|
|
want: peers(
|
|
n(1, "foo"),
|
|
n(2, "bar", seenAt(time.Unix(123, 0))),
|
|
),
|
|
wantStats: updateStats{changed: 2},
|
|
},
|
|
{
|
|
name: "ep_change_derp",
|
|
prev: peers(n(1, "foo", withDERP(3))),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
DERPRegion: 4,
|
|
}},
|
|
},
|
|
want: peers(n(1, "foo", withDERP(4))),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "ep_change_udp",
|
|
prev: peers(n(1, "foo", withEP("1.2.3.4:111"))),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
Endpoints: eps("1.2.3.4:56"),
|
|
}},
|
|
},
|
|
want: peers(n(1, "foo", withEP("1.2.3.4:56"))),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "ep_change_udp_2",
|
|
prev: peers(n(1, "foo", withDERP(3), withEP("1.2.3.4:111"))),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
Endpoints: eps("1.2.3.4:56"),
|
|
}},
|
|
},
|
|
want: peers(n(1, "foo", withDERP(3), withEP("1.2.3.4:56"))),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "ep_change_both",
|
|
prev: peers(n(1, "foo", withDERP(3), withEP("1.2.3.4:111"))),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
DERPRegion: 2,
|
|
Endpoints: eps("1.2.3.4:56"),
|
|
}},
|
|
},
|
|
want: peers(n(1, "foo", withDERP(2), withEP("1.2.3.4:56"))),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "change_key",
|
|
prev: peers(n(1, "foo")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
Key: new(key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
|
|
}},
|
|
}, want: peers(&tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo",
|
|
Key: key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))),
|
|
}),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "change_key_signature",
|
|
prev: peers(n(1, "foo")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
KeySignature: []byte{3, 4},
|
|
}},
|
|
},
|
|
want: peers(&tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo",
|
|
KeySignature: []byte{3, 4},
|
|
}),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "change_disco_key",
|
|
prev: peers(n(1, "foo")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
DiscoKey: new(key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
|
|
}},
|
|
},
|
|
want: peers(&tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo",
|
|
DiscoKey: key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))),
|
|
}),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "change_online",
|
|
prev: peers(n(1, "foo")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
Online: new(true),
|
|
}},
|
|
},
|
|
want: peers(&tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo",
|
|
Online: new(true),
|
|
}),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "change_last_seen",
|
|
prev: peers(n(1, "foo")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
LastSeen: new(time.Unix(123, 0).UTC()),
|
|
}},
|
|
},
|
|
want: peers(&tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo",
|
|
LastSeen: new(time.Unix(123, 0).UTC()),
|
|
}),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
{
|
|
name: "change_key_expiry",
|
|
prev: peers(n(1, "foo")),
|
|
mapRes: &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
KeyExpiry: new(time.Unix(123, 0).UTC()),
|
|
}},
|
|
},
|
|
want: peers(&tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo",
|
|
KeyExpiry: time.Unix(123, 0).UTC(),
|
|
}),
|
|
wantStats: updateStats{changed: 1},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if !tt.curTime.IsZero() {
|
|
curTime = tt.curTime
|
|
tstest.Replace(t, &clock, tstime.Clock(tstest.NewClock(tstest.ClockOpts{Start: curTime})))
|
|
}
|
|
ms := newTestMapSession(t, nil)
|
|
for _, n := range tt.prev {
|
|
mak.Set(&ms.peers, n.ID, n.View())
|
|
}
|
|
|
|
gotStats := ms.updatePeersStateFromResponse(tt.mapRes)
|
|
if gotStats != tt.wantStats {
|
|
t.Errorf("got stats = %+v; want %+v", gotStats, tt.wantStats)
|
|
}
|
|
|
|
var got []*tailcfg.Node
|
|
for _, vp := range ms.sortedPeers() {
|
|
got = append(got, vp.AsStruct())
|
|
}
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(got), formatNodes(tt.want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func formatNodes(nodes []*tailcfg.Node) string {
|
|
var sb strings.Builder
|
|
for i, n := range nodes {
|
|
if i > 0 {
|
|
sb.WriteString(", ")
|
|
}
|
|
fmt.Fprintf(&sb, "(%d, %q", n.ID, n.Name)
|
|
|
|
if n.Online != nil {
|
|
fmt.Fprintf(&sb, ", online=%v", *n.Online)
|
|
}
|
|
if n.LastSeen != nil {
|
|
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix())
|
|
}
|
|
if n.Key != (key.NodePublic{}) {
|
|
fmt.Fprintf(&sb, ", key=%v", n.Key.String())
|
|
}
|
|
if n.Expired {
|
|
fmt.Fprintf(&sb, ", expired=true")
|
|
}
|
|
sb.WriteString(")")
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func newTestMapSession(t testing.TB, nu NetmapUpdater) *mapSession {
|
|
ms := newMapSession(key.NewNode(), nu, new(controlknobs.Knobs))
|
|
t.Cleanup(ms.Close)
|
|
ms.logf = t.Logf
|
|
return ms
|
|
}
|
|
|
|
func (ms *mapSession) netmapForResponse(res *tailcfg.MapResponse) *netmap.NetworkMap {
|
|
ms.updateStateFromResponse(res)
|
|
return ms.netmap()
|
|
}
|
|
|
|
func TestNetmapForResponse(t *testing.T) {
|
|
t.Run("implicit_packetfilter", func(t *testing.T) {
|
|
somePacketFilter := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"*"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "10.2.3.4", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
},
|
|
},
|
|
}
|
|
ms := newTestMapSession(t, nil)
|
|
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
PacketFilter: somePacketFilter,
|
|
})
|
|
if len(nm1.PacketFilter) == 0 {
|
|
t.Fatalf("zero length PacketFilter")
|
|
}
|
|
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
PacketFilter: nil, // testing that the server can omit this.
|
|
})
|
|
if len(nm1.PacketFilter) == 0 {
|
|
t.Fatalf("zero length PacketFilter in 2nd netmap")
|
|
}
|
|
if !reflect.DeepEqual(nm1.PacketFilter, nm2.PacketFilter) {
|
|
t.Error("packet filters differ")
|
|
}
|
|
})
|
|
t.Run("implicit_dnsconfig", func(t *testing.T) {
|
|
someDNSConfig := &tailcfg.DNSConfig{Domains: []string{"foo", "bar"}}
|
|
ms := newTestMapSession(t, nil)
|
|
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
DNSConfig: someDNSConfig,
|
|
})
|
|
if !reflect.DeepEqual(nm1.DNS, *someDNSConfig) {
|
|
t.Fatalf("1st DNS wrong")
|
|
}
|
|
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
DNSConfig: nil, // implicit
|
|
})
|
|
if !reflect.DeepEqual(nm2.DNS, *someDNSConfig) {
|
|
t.Fatalf("2nd DNS wrong")
|
|
}
|
|
})
|
|
t.Run("collect_services", func(t *testing.T) {
|
|
ms := newTestMapSession(t, nil)
|
|
var nm *netmap.NetworkMap
|
|
wantCollect := func(v bool) {
|
|
t.Helper()
|
|
if nm.CollectServices != v {
|
|
t.Errorf("netmap.CollectServices = %v; want %v", nm.CollectServices, v)
|
|
}
|
|
}
|
|
|
|
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
})
|
|
wantCollect(false)
|
|
|
|
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
CollectServices: "false",
|
|
})
|
|
wantCollect(false)
|
|
|
|
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
CollectServices: "true",
|
|
})
|
|
wantCollect(true)
|
|
|
|
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
CollectServices: "",
|
|
})
|
|
wantCollect(true)
|
|
})
|
|
t.Run("implicit_domain", func(t *testing.T) {
|
|
ms := newTestMapSession(t, nil)
|
|
var nm *netmap.NetworkMap
|
|
want := func(v string) {
|
|
t.Helper()
|
|
if nm.Domain != v {
|
|
t.Errorf("netmap.Domain = %q; want %q", nm.Domain, v)
|
|
}
|
|
}
|
|
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
Domain: "foo.com",
|
|
})
|
|
want("foo.com")
|
|
|
|
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
})
|
|
want("foo.com")
|
|
})
|
|
t.Run("implicit_node", func(t *testing.T) {
|
|
someNode := &tailcfg.Node{
|
|
Name: "foo",
|
|
}
|
|
wantNode := (&tailcfg.Node{
|
|
Name: "foo",
|
|
ComputedName: "foo",
|
|
ComputedNameWithHost: "foo",
|
|
}).View()
|
|
ms := newTestMapSession(t, nil)
|
|
mapRes := &tailcfg.MapResponse{
|
|
Node: someNode,
|
|
}
|
|
initDisplayNames(mapRes.Node.View(), mapRes)
|
|
ms.updateStateFromResponse(mapRes)
|
|
nm1 := ms.netmap()
|
|
if !nm1.SelfNode.Valid() {
|
|
t.Fatal("nil Node in 1st netmap")
|
|
}
|
|
if !reflect.DeepEqual(nm1.SelfNode, wantNode) {
|
|
j, _ := json.Marshal(nm1.SelfNode)
|
|
t.Errorf("Node mismatch in 1st netmap; got: %s", j)
|
|
}
|
|
|
|
ms.updateStateFromResponse(&tailcfg.MapResponse{})
|
|
nm2 := ms.netmap()
|
|
if !nm2.SelfNode.Valid() {
|
|
t.Fatal("nil Node in 1st netmap")
|
|
}
|
|
if !reflect.DeepEqual(nm2.SelfNode, wantNode) {
|
|
j, _ := json.Marshal(nm2.SelfNode)
|
|
t.Errorf("Node mismatch in 2nd netmap; got: %s", j)
|
|
}
|
|
})
|
|
t.Run("named_packetfilter", func(t *testing.T) {
|
|
pfA := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"10.0.0.1"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "10.2.3.4", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
},
|
|
},
|
|
}
|
|
pfB := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"10.0.0.2"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "10.2.3.4", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
},
|
|
},
|
|
}
|
|
ms := newTestMapSession(t, nil)
|
|
|
|
// Mix of old & new style (PacketFilter and PacketFilters).
|
|
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
PacketFilter: pfA,
|
|
PacketFilters: map[string][]tailcfg.FilterRule{
|
|
"pf-b": pfB,
|
|
},
|
|
})
|
|
if got, want := len(nm1.PacketFilter), 2; got != want {
|
|
t.Fatalf("PacketFilter length = %v; want %v", got, want)
|
|
}
|
|
if got, want := first(nm1.PacketFilter[0].Srcs).String(), "10.0.0.1/32"; got != want {
|
|
t.Fatalf("PacketFilter[0].Srcs = %v; want %v", got, want)
|
|
}
|
|
if got, want := first(nm1.PacketFilter[1].Srcs).String(), "10.0.0.2/32"; got != want {
|
|
t.Fatalf("PacketFilter[0].Srcs = %v; want %v", got, want)
|
|
}
|
|
|
|
// No-op change. Remember the old stuff.
|
|
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
PacketFilter: nil,
|
|
PacketFilters: nil,
|
|
})
|
|
if got, want := len(nm2.PacketFilter), 2; got != want {
|
|
t.Fatalf("PacketFilter length = %v; want %v", got, want)
|
|
}
|
|
if !reflect.DeepEqual(nm1.PacketFilter, nm2.PacketFilter) {
|
|
t.Error("packet filters differ")
|
|
}
|
|
|
|
// New style only, with clear.
|
|
nm3 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
PacketFilter: nil,
|
|
PacketFilters: map[string][]tailcfg.FilterRule{
|
|
"*": nil,
|
|
"pf-b": pfB,
|
|
},
|
|
})
|
|
if got, want := len(nm3.PacketFilter), 1; got != want {
|
|
t.Fatalf("PacketFilter length = %v; want %v", got, want)
|
|
}
|
|
if got, want := first(nm3.PacketFilter[0].Srcs).String(), "10.0.0.2/32"; got != want {
|
|
t.Fatalf("PacketFilter[0].Srcs = %v; want %v", got, want)
|
|
}
|
|
|
|
// New style only, adding pfA back, not as the legacy "base" layer:.
|
|
nm4 := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
PacketFilter: nil,
|
|
PacketFilters: map[string][]tailcfg.FilterRule{
|
|
"pf-a": pfA,
|
|
},
|
|
})
|
|
if got, want := len(nm4.PacketFilter), 2; got != want {
|
|
t.Fatalf("PacketFilter length = %v; want %v", got, want)
|
|
}
|
|
if got, want := first(nm4.PacketFilter[0].Srcs).String(), "10.0.0.1/32"; got != want {
|
|
t.Fatalf("PacketFilter[0].Srcs = %v; want %v", got, want)
|
|
}
|
|
if got, want := first(nm4.PacketFilter[1].Srcs).String(), "10.0.0.2/32"; got != want {
|
|
t.Fatalf("PacketFilter[0].Srcs = %v; want %v", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateDiscoForNode(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialOnline bool
|
|
initialLastSeen time.Time
|
|
updateDiscoKey func() key.DiscoPublic
|
|
updateOnline bool
|
|
updateLastSeen time.Time
|
|
wantUpdate bool
|
|
wantKeyChanged bool
|
|
}{
|
|
{
|
|
name: "newer_key_not_online",
|
|
initialOnline: true,
|
|
initialLastSeen: time.Unix(1, 0),
|
|
updateDiscoKey: key.NewDisco().Public,
|
|
updateOnline: false,
|
|
updateLastSeen: time.Now(),
|
|
wantUpdate: true,
|
|
wantKeyChanged: true,
|
|
},
|
|
{
|
|
name: "newer_key_online",
|
|
initialOnline: true,
|
|
initialLastSeen: time.Unix(1, 0),
|
|
updateDiscoKey: key.NewDisco().Public,
|
|
updateOnline: true,
|
|
updateLastSeen: time.Now(),
|
|
wantUpdate: true,
|
|
wantKeyChanged: true,
|
|
},
|
|
{
|
|
name: "older_key_not_online",
|
|
initialOnline: false,
|
|
initialLastSeen: time.Now(),
|
|
updateDiscoKey: key.NewDisco().Public,
|
|
updateOnline: false,
|
|
updateLastSeen: time.Unix(1, 0),
|
|
wantUpdate: false,
|
|
wantKeyChanged: false,
|
|
},
|
|
{
|
|
name: "older_key_online",
|
|
initialOnline: false,
|
|
initialLastSeen: time.Now(),
|
|
updateDiscoKey: key.NewDisco().Public,
|
|
updateOnline: true,
|
|
updateLastSeen: time.Unix(1, 0),
|
|
wantUpdate: true,
|
|
wantKeyChanged: true,
|
|
},
|
|
{
|
|
name: "same_newer_key_not_online",
|
|
initialOnline: true,
|
|
initialLastSeen: time.Unix(1, 0),
|
|
updateDiscoKey: nil,
|
|
updateOnline: false,
|
|
updateLastSeen: time.Now(),
|
|
wantUpdate: false,
|
|
wantKeyChanged: false,
|
|
},
|
|
{
|
|
name: "same_newer_key_online",
|
|
initialOnline: true,
|
|
initialLastSeen: time.Unix(1, 0),
|
|
updateDiscoKey: nil,
|
|
updateOnline: true,
|
|
updateLastSeen: time.Now(),
|
|
wantUpdate: false,
|
|
wantKeyChanged: false,
|
|
},
|
|
{
|
|
name: "same_older_key_not_online",
|
|
initialOnline: false,
|
|
initialLastSeen: time.Now(),
|
|
updateDiscoKey: nil,
|
|
updateOnline: false,
|
|
updateLastSeen: time.Unix(1, 0),
|
|
wantUpdate: false,
|
|
wantKeyChanged: false,
|
|
},
|
|
{
|
|
name: "same_older_key_online",
|
|
initialOnline: false,
|
|
initialLastSeen: time.Now(),
|
|
updateDiscoKey: nil,
|
|
updateOnline: true,
|
|
updateLastSeen: time.Unix(1, 0),
|
|
wantUpdate: true,
|
|
wantKeyChanged: false,
|
|
},
|
|
{
|
|
name: "no_initial_last_seen",
|
|
initialOnline: false,
|
|
updateDiscoKey: key.NewDisco().Public,
|
|
updateOnline: false,
|
|
updateLastSeen: time.Now(),
|
|
wantUpdate: true,
|
|
wantKeyChanged: true,
|
|
},
|
|
{
|
|
name: "zero_key",
|
|
initialOnline: false,
|
|
updateDiscoKey: func() key.DiscoPublic {
|
|
return key.DiscoPublic{}
|
|
},
|
|
updateOnline: false,
|
|
updateLastSeen: time.Now(),
|
|
wantUpdate: false,
|
|
wantKeyChanged: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
synctest.Test(t, func(*testing.T) {
|
|
nu := &rememberLastNetmapUpdater{
|
|
done: make(chan any, 1),
|
|
}
|
|
ms := newTestMapSession(t, nu)
|
|
defer ms.Close()
|
|
|
|
oldKey := key.NewDisco()
|
|
|
|
// Insert existing node
|
|
node := tailcfg.Node{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: oldKey.Public(),
|
|
Online: &tt.initialOnline,
|
|
}
|
|
if !tt.initialLastSeen.IsZero() {
|
|
node.LastSeen = &tt.initialLastSeen
|
|
}
|
|
|
|
if nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Peers: []*tailcfg.Node{&node},
|
|
}); len(nm.Peers) != 1 {
|
|
t.Fatalf("node not inserted")
|
|
}
|
|
|
|
newKey := oldKey.Public()
|
|
if tt.updateDiscoKey != nil {
|
|
newKey = tt.updateDiscoKey()
|
|
}
|
|
ms.updateDiscoForNode(node.ID, node.Key, newKey, tt.updateLastSeen, tt.updateOnline)
|
|
|
|
// We have an early escape that would not trigger the netmap updater.
|
|
synctest.Wait()
|
|
select {
|
|
case <-nu.done:
|
|
if !tt.wantUpdate {
|
|
t.Errorf("did not expect update, got: %v", nu.last)
|
|
}
|
|
default:
|
|
if tt.wantUpdate {
|
|
t.Errorf("expected update, did not get any")
|
|
}
|
|
}
|
|
|
|
peer, ok := ms.peers[node.ID]
|
|
if !ok {
|
|
t.Fatal("node not found")
|
|
}
|
|
|
|
keyChanged := peer.DiscoKey().Compare(oldKey.Public()) != 0
|
|
if keyChanged != tt.wantKeyChanged {
|
|
t.Errorf("Disco key update: %t, wanted update: %t", keyChanged, tt.wantKeyChanged)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateDiscoForNodeCallback(t *testing.T) {
|
|
t.Run("key_wired_through_to_updater", func(t *testing.T) {
|
|
nu := &rememberLastNetmapUpdater{
|
|
done: make(chan any, 1),
|
|
}
|
|
ms := newTestMapSession(t, nu)
|
|
|
|
oldKey := key.NewDisco()
|
|
|
|
// Insert existing node
|
|
node := tailcfg.Node{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: oldKey.Public(),
|
|
Online: new(false),
|
|
LastSeen: new(time.Unix(1, 0)),
|
|
}
|
|
|
|
if nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Peers: []*tailcfg.Node{&node},
|
|
}); len(nm.Peers) != 1 {
|
|
t.Fatalf("node not inserted")
|
|
}
|
|
|
|
newKey := key.NewDisco()
|
|
ms.updateDiscoForNode(node.ID, node.Key, newKey.Public(), time.Now(), false)
|
|
<-nu.done
|
|
|
|
if nu.lastTSMPKey != node.Key || nu.lastTSMPDisco != newKey.Public() {
|
|
t.Fatalf("expected [%s]=%s, got [%s]=%s", node.Key, newKey.Public(),
|
|
nu.lastTSMPKey, nu.lastTSMPDisco)
|
|
}
|
|
})
|
|
// Even though key stays in list of update, the updater only triggers on TSMP.
|
|
t.Run("key_not_wired_through_to_updater", func(t *testing.T) {
|
|
nu := &rememberLastNetmapUpdater{
|
|
done: make(chan any, 1),
|
|
}
|
|
ms := newTestMapSession(t, nu)
|
|
|
|
oldKey := key.NewDisco()
|
|
|
|
// Insert existing node
|
|
node := tailcfg.Node{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: oldKey.Public(),
|
|
Online: new(false),
|
|
LastSeen: new(time.Unix(1, 0)),
|
|
}
|
|
|
|
if nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Peers: []*tailcfg.Node{&node},
|
|
}); len(nm.Peers) != 1 {
|
|
t.Fatalf("node not inserted")
|
|
}
|
|
|
|
newKey := key.NewDisco().Public()
|
|
resp := &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: node.ID,
|
|
Key: &node.Key,
|
|
LastSeen: new(time.Now()),
|
|
Online: new(true),
|
|
DiscoKey: &newKey,
|
|
}},
|
|
}
|
|
// Not TSMP Path, just regular injection path.
|
|
ms.HandleNonKeepAliveMapResponse(t.Context(), resp)
|
|
<-nu.done
|
|
|
|
if !nu.lastTSMPKey.IsZero() || !nu.lastTSMPDisco.IsZero() {
|
|
t.Fatalf("expected zero keys, got [%s]=%s",
|
|
nu.lastTSMPKey, nu.lastTSMPDisco)
|
|
}
|
|
})
|
|
|
|
t.Run("test_deadlock", func(t *testing.T) {
|
|
nu := &rememberLastNetmapUpdater{
|
|
done: make(chan any, 1),
|
|
}
|
|
ms := newTestMapSession(t, nu)
|
|
// Very barebones onDebug func that will let us exercise sleep command
|
|
// from control and potentially induce deadlocks.
|
|
ms.onDebug = func(ctx context.Context, d *tailcfg.Debug) error {
|
|
time.Sleep(time.Duration(d.SleepSeconds * float64(time.Second)))
|
|
return nil
|
|
}
|
|
|
|
oldKey := key.NewDisco()
|
|
|
|
// Insert existing node
|
|
node := tailcfg.Node{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: oldKey.Public(),
|
|
Online: new(false),
|
|
LastSeen: new(time.Unix(1, 0)),
|
|
}
|
|
|
|
if nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Peers: []*tailcfg.Node{&node},
|
|
}); len(nm.Peers) != 1 {
|
|
t.Fatalf("node not inserted")
|
|
}
|
|
|
|
sleep1 := &tailcfg.MapResponse{
|
|
Debug: &tailcfg.Debug{
|
|
SleepSeconds: 1.0,
|
|
},
|
|
}
|
|
ms.HandleNonKeepAliveMapResponse(t.Context(), sleep1)
|
|
|
|
// Resembles the disco key advert subscriber running in a separate context.
|
|
go func() {
|
|
newKey := key.NewDisco()
|
|
ms.updateDiscoForNode(node.ID, node.Key, newKey.Public(), time.Now(), false)
|
|
}()
|
|
|
|
ms.Close()
|
|
|
|
<-nu.done
|
|
})
|
|
}
|
|
|
|
func TestUpdateDiscoForNodeCallbackWithFullNetmap(t *testing.T) {
|
|
now := time.Now()
|
|
oldTime := time.Unix(1, 0)
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialOnline bool
|
|
initialLastSeen time.Time
|
|
updateOnline bool
|
|
updateLastSeen time.Time
|
|
expectNewDisco bool
|
|
}{
|
|
{
|
|
name: "disco-key-newer-lastSeen",
|
|
initialOnline: false,
|
|
initialLastSeen: oldTime,
|
|
updateOnline: false,
|
|
updateLastSeen: now,
|
|
expectNewDisco: true,
|
|
},
|
|
{
|
|
name: "disco-key-older-lastSeen",
|
|
initialOnline: false,
|
|
initialLastSeen: now,
|
|
updateOnline: false,
|
|
updateLastSeen: oldTime,
|
|
expectNewDisco: false,
|
|
},
|
|
{
|
|
name: "disco-key-newer-lastSeen-going-offline",
|
|
initialOnline: true,
|
|
initialLastSeen: oldTime,
|
|
updateOnline: false,
|
|
updateLastSeen: now,
|
|
expectNewDisco: true,
|
|
},
|
|
{
|
|
name: "online-flip-newer-lastSeen",
|
|
initialOnline: false,
|
|
initialLastSeen: oldTime,
|
|
updateOnline: true,
|
|
updateLastSeen: now,
|
|
expectNewDisco: true,
|
|
},
|
|
{
|
|
name: "local-lastseen-preserved-after-first-reconnect",
|
|
initialOnline: false,
|
|
initialLastSeen: now,
|
|
updateOnline: false,
|
|
updateLastSeen: now,
|
|
expectNewDisco: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
nu := &rememberLastNetmapUpdater{
|
|
done: make(chan any, 1),
|
|
}
|
|
ms := newTestMapSession(t, nu)
|
|
|
|
oldKey := key.NewDisco()
|
|
|
|
// Initial node
|
|
node := tailcfg.Node{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: oldKey.Public(),
|
|
Online: new(tt.initialOnline),
|
|
LastSeen: new(tt.initialLastSeen),
|
|
Name: "host.network.ts.net",
|
|
}
|
|
|
|
if nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Peers: []*tailcfg.Node{&node},
|
|
}); len(nm.Peers) != 1 {
|
|
t.Fatalf("node not inserted")
|
|
}
|
|
|
|
newKey := key.NewDisco()
|
|
|
|
// Updated node
|
|
newNode := tailcfg.Node{
|
|
ID: 1,
|
|
Key: node.Key,
|
|
DiscoKey: newKey.Public(),
|
|
Online: new(tt.updateOnline),
|
|
LastSeen: new(tt.updateLastSeen),
|
|
Name: "host.network.ts.net",
|
|
}
|
|
|
|
ms.HandleNonKeepAliveMapResponse(t.Context(), &tailcfg.MapResponse{
|
|
Node: &newNode,
|
|
Peers: []*tailcfg.Node{
|
|
&newNode,
|
|
},
|
|
})
|
|
<-nu.done
|
|
|
|
newMap := nu.last
|
|
if n := len(newMap.Peers); n != 1 {
|
|
t.Fatalf("netmap not right length, got %d, expected %d", n, 1)
|
|
}
|
|
|
|
peer := newMap.Peers[0]
|
|
|
|
expectedDisco := oldKey.Public()
|
|
if tt.expectNewDisco {
|
|
expectedDisco = newKey.Public()
|
|
}
|
|
|
|
if peer.Key() != node.Key || peer.DiscoKey() != expectedDisco {
|
|
t.Fatalf("expected [%s]=%s, got [%s]=%s",
|
|
node.Key, expectedDisco,
|
|
peer.Key(), peer.DiscoKey(),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func first[T any](s []T) T {
|
|
if len(s) == 0 {
|
|
var zero T
|
|
return zero
|
|
}
|
|
return s[0]
|
|
}
|
|
|
|
func TestDeltaDERPMap(t *testing.T) {
|
|
regions1 := map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
Nodes: []*tailcfg.DERPNode{{
|
|
Name: "derp1a",
|
|
RegionID: 1,
|
|
HostName: "derp1a" + tailcfg.DotInvalid,
|
|
IPv4: "169.254.169.254",
|
|
IPv6: "none",
|
|
}},
|
|
},
|
|
}
|
|
|
|
// As above, but with a changed IPv4 addr
|
|
regions2 := map[int]*tailcfg.DERPRegion{1: regions1[1].Clone()}
|
|
regions2[1].Nodes[0].IPv4 = "127.0.0.1"
|
|
|
|
type step struct {
|
|
got *tailcfg.DERPMap
|
|
want *tailcfg.DERPMap
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
steps []step
|
|
}{
|
|
{
|
|
name: "nothing-to-nothing",
|
|
steps: []step{
|
|
{nil, nil},
|
|
{nil, nil},
|
|
},
|
|
},
|
|
{
|
|
name: "regions-sticky",
|
|
steps: []step{
|
|
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
|
|
{&tailcfg.DERPMap{}, &tailcfg.DERPMap{Regions: regions1}},
|
|
},
|
|
},
|
|
{
|
|
name: "regions-change",
|
|
steps: []step{
|
|
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
|
|
{&tailcfg.DERPMap{Regions: regions2}, &tailcfg.DERPMap{Regions: regions2}},
|
|
},
|
|
},
|
|
{
|
|
name: "home-params",
|
|
steps: []step{
|
|
// Send a DERP map
|
|
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
|
|
// Send home params, want to still have the same regions
|
|
{
|
|
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{
|
|
RegionScore: map[int]float64{1: 0.5},
|
|
}},
|
|
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
|
|
RegionScore: map[int]float64{1: 0.5},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "home-params-sub-fields",
|
|
steps: []step{
|
|
// Send a DERP map with home params
|
|
{
|
|
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
|
|
RegionScore: map[int]float64{1: 0.5},
|
|
}},
|
|
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
|
|
RegionScore: map[int]float64{1: 0.5},
|
|
}},
|
|
},
|
|
// Sending a struct with a 'HomeParams' field but nil RegionScore doesn't change home params...
|
|
{
|
|
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{RegionScore: nil}},
|
|
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
|
|
RegionScore: map[int]float64{1: 0.5},
|
|
}},
|
|
},
|
|
// ... but sending one with a non-nil and empty RegionScore field zeroes that out.
|
|
{
|
|
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{RegionScore: map[int]float64{}}},
|
|
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
|
|
RegionScore: map[int]float64{},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ms := newTestMapSession(t, nil)
|
|
for stepi, s := range tt.steps {
|
|
nm := ms.netmapForResponse(&tailcfg.MapResponse{DERPMap: s.got})
|
|
if !reflect.DeepEqual(nm.DERPMap, s.want) {
|
|
t.Errorf("unexpected result at step index %v; got: %s", stepi, logger.AsJSON(nm.DERPMap))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPeerChangeDiff(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
a, b *tailcfg.Node
|
|
want *tailcfg.PeerChange // nil means want ok=false, unless wantEqual is set
|
|
wantEqual bool // means test wants (nil, true)
|
|
}{
|
|
{
|
|
name: "eq",
|
|
a: &tailcfg.Node{ID: 1},
|
|
b: &tailcfg.Node{ID: 1},
|
|
wantEqual: true,
|
|
},
|
|
{
|
|
name: "patch-derp",
|
|
a: &tailcfg.Node{ID: 1, HomeDERP: 1},
|
|
b: &tailcfg.Node{ID: 1, HomeDERP: 2},
|
|
want: &tailcfg.PeerChange{NodeID: 1, DERPRegion: 2},
|
|
},
|
|
{
|
|
name: "patch-endpoints",
|
|
a: &tailcfg.Node{ID: 1, Endpoints: eps("10.0.0.1:1")},
|
|
b: &tailcfg.Node{ID: 1, Endpoints: eps("10.0.0.2:2")},
|
|
want: &tailcfg.PeerChange{NodeID: 1, Endpoints: eps("10.0.0.2:2")},
|
|
},
|
|
{
|
|
name: "patch-cap",
|
|
a: &tailcfg.Node{ID: 1, Cap: 1},
|
|
b: &tailcfg.Node{ID: 1, Cap: 2},
|
|
want: &tailcfg.PeerChange{NodeID: 1, Cap: 2},
|
|
},
|
|
{
|
|
name: "patch-lastseen",
|
|
a: &tailcfg.Node{ID: 1, LastSeen: new(time.Unix(1, 0))},
|
|
b: &tailcfg.Node{ID: 1, LastSeen: new(time.Unix(2, 0))},
|
|
want: &tailcfg.PeerChange{NodeID: 1, LastSeen: new(time.Unix(2, 0))},
|
|
},
|
|
{
|
|
name: "patch-online-to-true",
|
|
a: &tailcfg.Node{ID: 1, Online: new(false)},
|
|
b: &tailcfg.Node{ID: 1, Online: new(true)},
|
|
want: &tailcfg.PeerChange{NodeID: 1, Online: new(true)},
|
|
},
|
|
{
|
|
name: "patch-online-to-false",
|
|
a: &tailcfg.Node{ID: 1, Online: new(true)},
|
|
b: &tailcfg.Node{ID: 1, Online: new(false)},
|
|
want: &tailcfg.PeerChange{NodeID: 1, Online: new(false)},
|
|
},
|
|
{
|
|
name: "mix-patchable-and-not",
|
|
a: &tailcfg.Node{ID: 1, Cap: 1},
|
|
b: &tailcfg.Node{ID: 1, Cap: 2, StableID: "foo"},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "miss-change-stableid",
|
|
a: &tailcfg.Node{ID: 1},
|
|
b: &tailcfg.Node{ID: 1, StableID: "diff"},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "miss-change-id",
|
|
a: &tailcfg.Node{ID: 1},
|
|
b: &tailcfg.Node{ID: 2},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "miss-change-name",
|
|
a: &tailcfg.Node{ID: 1, Name: "foo"},
|
|
b: &tailcfg.Node{ID: 1, Name: "bar"},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "miss-change-user",
|
|
a: &tailcfg.Node{ID: 1, User: 1},
|
|
b: &tailcfg.Node{ID: 1, User: 2},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "miss-change-masq-v4",
|
|
a: &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: new(netip.MustParseAddr("100.64.0.1"))},
|
|
b: &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: new(netip.MustParseAddr("100.64.0.2"))},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "miss-change-masq-v6",
|
|
a: &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: new(netip.MustParseAddr("2001::3456"))},
|
|
b: &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: new(netip.MustParseAddr("2001::3006"))},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "patch-capmap-add-value-to-existing-key",
|
|
a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: []tailcfg.RawMessage{"true"}}},
|
|
want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: []tailcfg.RawMessage{"true"}}},
|
|
},
|
|
{
|
|
name: "patch-capmap-add-new-key",
|
|
a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil, tailcfg.CapabilityDebug: nil}},
|
|
want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil, tailcfg.CapabilityDebug: nil}},
|
|
},
|
|
{
|
|
name: "patch-capmap-remove-key",
|
|
a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{}},
|
|
want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{}},
|
|
},
|
|
{
|
|
name: "patch-capmap-remove-as-nil",
|
|
a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
b: &tailcfg.Node{ID: 1},
|
|
want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{}},
|
|
},
|
|
{
|
|
name: "patch-capmap-add-key-to-empty-map",
|
|
a: &tailcfg.Node{ID: 1},
|
|
b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
},
|
|
{
|
|
name: "patch-capmap-no-change",
|
|
a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}},
|
|
wantEqual: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pc, ok := peerChangeDiff(tt.a.View(), tt.b, nil)
|
|
if tt.wantEqual {
|
|
if !ok || pc != nil {
|
|
t.Errorf("got (%p, %v); want (nil, true); pc=%v", pc, ok, logger.AsJSON(pc))
|
|
}
|
|
return
|
|
}
|
|
if (pc != nil) != ok {
|
|
t.Fatalf("inconsistent ok=%v, pc=%p", ok, pc)
|
|
}
|
|
if !reflect.DeepEqual(pc, tt.want) {
|
|
t.Errorf("mismatch\n got: %v\nwant: %v\n", logger.AsJSON(pc), logger.AsJSON(tt.want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPeerChangeDiffAllocs(t *testing.T) {
|
|
a := &tailcfg.Node{ID: 1}
|
|
b := &tailcfg.Node{ID: 1}
|
|
n := testing.AllocsPerRun(10000, func() {
|
|
diff, ok := peerChangeDiff(a.View(), b, nil)
|
|
if !ok || diff != nil {
|
|
t.Fatalf("unexpected result: (%s, %v)", logger.AsJSON(diff), ok)
|
|
}
|
|
})
|
|
if n != 0 {
|
|
t.Errorf("allocs = %v; want 0", int(n))
|
|
}
|
|
}
|
|
|
|
type countingNetmapUpdater struct {
|
|
full atomic.Int64
|
|
}
|
|
|
|
func (nu *countingNetmapUpdater) UpdateFullNetmap(nm *netmap.NetworkMap) {
|
|
nu.full.Add(1)
|
|
}
|
|
|
|
type countingDeltaNetmapUpdater struct {
|
|
countingNetmapUpdater
|
|
delta atomic.Int64
|
|
}
|
|
|
|
func (nu *countingDeltaNetmapUpdater) UpdateNetmapDelta([]netmap.NodeMutation) bool {
|
|
nu.delta.Add(1)
|
|
return true
|
|
}
|
|
|
|
func TestExistingPeerReplacementHandledIncrementally(t *testing.T) {
|
|
nu := &countingDeltaNetmapUpdater{}
|
|
ms := newTestMapSession(t, nu)
|
|
ctx := t.Context()
|
|
|
|
peer := &tailcfg.Node{
|
|
ID: 1,
|
|
StableID: "peer",
|
|
Name: "peer.example.ts.net.",
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: key.NewDisco().Public(),
|
|
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
|
AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
|
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
|
}
|
|
if err := ms.handleNonKeepAliveMapResponse(ctx, &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{Name: "self.example.ts.net."},
|
|
Peers: []*tailcfg.Node{peer},
|
|
}, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := nu.full.Load(); got != 1 {
|
|
t.Fatalf("full updates after initial response = %d; want 1", got)
|
|
}
|
|
|
|
replacement := peer.Clone()
|
|
replacement.AllowedIPs = append(replacement.AllowedIPs, netip.MustParsePrefix("100.64.0.2/32"))
|
|
if err := ms.handleNonKeepAliveMapResponse(ctx, &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{replacement},
|
|
}, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := nu.full.Load(); got != 1 {
|
|
t.Errorf("full updates after route-changing peer replacement = %d; want 1", got)
|
|
}
|
|
if got := nu.delta.Load(); got != 1 {
|
|
t.Errorf("delta updates after route-changing peer replacement = %d; want 1", got)
|
|
}
|
|
}
|
|
|
|
type profileRecordingUpdater struct {
|
|
countingDeltaNetmapUpdater
|
|
profiles []map[tailcfg.UserID]tailcfg.UserProfileView
|
|
profilesAtDelta int
|
|
}
|
|
|
|
func (nu *profileRecordingUpdater) UpdateUserProfiles(profiles map[tailcfg.UserID]tailcfg.UserProfileView) bool {
|
|
nu.profiles = append(nu.profiles, profiles)
|
|
return true
|
|
}
|
|
|
|
func (nu *profileRecordingUpdater) UpdateNetmapDelta(muts []netmap.NodeMutation) bool {
|
|
nu.profilesAtDelta = len(nu.profiles)
|
|
return nu.countingDeltaNetmapUpdater.UpdateNetmapDelta(muts)
|
|
}
|
|
|
|
// TestUpsertReplaysUserProfiles verifies that a peer upsert delivered as a
|
|
// delta also replays the peer's user and sharer profiles from the map
|
|
// session's profile store, even when the MapResponse carries no UserProfiles
|
|
// (control only resends changed profiles). A full netmap installed while the
|
|
// user had no visible peers drops the profile downstream, and without the
|
|
// replay a WhoIs on the returned peer fails at the user profile lookup.
|
|
func TestUpsertReplaysUserProfiles(t *testing.T) {
|
|
nu := &profileRecordingUpdater{}
|
|
ms := newTestMapSession(t, nu)
|
|
ctx := t.Context()
|
|
|
|
peer := &tailcfg.Node{
|
|
ID: 1,
|
|
StableID: "peer",
|
|
Name: "peer.example.ts.net.",
|
|
User: 100,
|
|
Sharer: 200,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: key.NewDisco().Public(),
|
|
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
|
AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
|
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
|
}
|
|
if err := ms.handleNonKeepAliveMapResponse(ctx, &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{Name: "self.example.ts.net."},
|
|
Peers: []*tailcfg.Node{peer},
|
|
UserProfiles: []tailcfg.UserProfile{
|
|
{ID: 0, LoginName: "invalid@example.com"},
|
|
{ID: 100, LoginName: "user@example.com"},
|
|
{ID: 200, LoginName: "sharer@example.com"},
|
|
},
|
|
}, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := nu.full.Load(); got != 1 {
|
|
t.Fatalf("full updates after initial response = %d; want 1", got)
|
|
}
|
|
|
|
// An upsert with no UserProfiles in the response must still deliver
|
|
// both profiles, before the delta lands.
|
|
replacement := peer.Clone()
|
|
replacement.AllowedIPs = append(replacement.AllowedIPs, netip.MustParsePrefix("100.64.0.2/32"))
|
|
if err := ms.handleNonKeepAliveMapResponse(ctx, &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{replacement},
|
|
}, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := nu.full.Load(); got != 1 {
|
|
t.Fatalf("full updates after peer upsert = %d; want 1", got)
|
|
}
|
|
if got := nu.delta.Load(); got != 1 {
|
|
t.Fatalf("delta updates after peer upsert = %d; want 1", got)
|
|
}
|
|
if got := len(nu.profiles); got != 2 {
|
|
t.Fatalf("UpdateUserProfiles calls = %d; want 2 (one initial, one replayed)", got)
|
|
}
|
|
if got := nu.profilesAtDelta; got != 2 {
|
|
t.Errorf("profiles delivered before delta = %d; want 2", got)
|
|
}
|
|
replayed := nu.profiles[1]
|
|
if _, ok := replayed[0]; ok {
|
|
t.Error("replayed profiles contains zero user ID")
|
|
}
|
|
for _, id := range []tailcfg.UserID{100, 200} {
|
|
up, ok := replayed[id]
|
|
if !ok || !up.Valid() {
|
|
t.Errorf("replayed profiles missing valid profile for user %d", id)
|
|
}
|
|
}
|
|
|
|
// A patch-only change (no upsert) must not replay any profiles.
|
|
patched := replacement.Clone()
|
|
patched.Endpoints = eps("10.0.0.1:1111")
|
|
if err := ms.handleNonKeepAliveMapResponse(ctx, &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{patched},
|
|
}, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := len(nu.profiles); got != 2 {
|
|
t.Errorf("UpdateUserProfiles calls after patch-only change = %d; want still 2", got)
|
|
}
|
|
}
|
|
|
|
// tests (*mapSession).patchifyPeersChanged; smaller tests are in TestPeerChangeDiff
|
|
func TestPatchifyPeersChanged(t *testing.T) {
|
|
hi := (&tailcfg.Hostinfo{}).View()
|
|
tests := []struct {
|
|
name string
|
|
mr0 *tailcfg.MapResponse // initial
|
|
mr1 *tailcfg.MapResponse // incremental
|
|
want *tailcfg.MapResponse // what the incremental one should've been mutated to
|
|
}{
|
|
{
|
|
name: "change_one_endpoint",
|
|
mr0: &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
|
|
Peers: []*tailcfg.Node{
|
|
{ID: 1, Hostinfo: hi},
|
|
},
|
|
},
|
|
mr1: &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{
|
|
{ID: 1, Endpoints: eps("10.0.0.1:1111"), Hostinfo: hi},
|
|
},
|
|
},
|
|
want: &tailcfg.MapResponse{
|
|
PeersChanged: nil,
|
|
PeersChangedPatch: []*tailcfg.PeerChange{
|
|
{NodeID: 1, Endpoints: eps("10.0.0.1:1111")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "change_some",
|
|
mr0: &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
|
|
Peers: []*tailcfg.Node{
|
|
{ID: 1, HomeDERP: 1, Hostinfo: hi},
|
|
{ID: 2, HomeDERP: 2, Hostinfo: hi},
|
|
{ID: 3, HomeDERP: 3, Hostinfo: hi},
|
|
},
|
|
},
|
|
mr1: &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{
|
|
{ID: 1, HomeDERP: 11, Hostinfo: hi},
|
|
{ID: 2, StableID: "other-change", Hostinfo: hi},
|
|
{ID: 3, HomeDERP: 33, Hostinfo: hi},
|
|
{ID: 4, HomeDERP: 4, Hostinfo: hi},
|
|
},
|
|
},
|
|
want: &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{
|
|
{ID: 2, StableID: "other-change", Hostinfo: hi},
|
|
{ID: 4, HomeDERP: 4, Hostinfo: hi},
|
|
},
|
|
PeersChangedPatch: []*tailcfg.PeerChange{
|
|
{NodeID: 1, DERPRegion: 11},
|
|
{NodeID: 3, DERPRegion: 33},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "change_exitnodednsresolvers",
|
|
mr0: &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
|
|
Peers: []*tailcfg.Node{
|
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
|
|
},
|
|
},
|
|
mr1: &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{
|
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
|
|
},
|
|
},
|
|
want: &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{
|
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "same_exitnoderesolvers",
|
|
mr0: &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
|
|
Peers: []*tailcfg.Node{
|
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
|
|
},
|
|
},
|
|
mr1: &tailcfg.MapResponse{
|
|
PeersChanged: []*tailcfg.Node{
|
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
|
|
},
|
|
},
|
|
want: &tailcfg.MapResponse{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
nu := &countingNetmapUpdater{}
|
|
ms := newTestMapSession(t, nu)
|
|
ms.updateStateFromResponse(tt.mr0)
|
|
mr1 := new(tailcfg.MapResponse)
|
|
must.Do(json.Unmarshal(must.Get(json.Marshal(tt.mr1)), mr1))
|
|
ms.patchifyPeersChanged(mr1)
|
|
opts := []cmp.Option{
|
|
cmp.Comparer(func(a, b netip.AddrPort) bool { return a == b }),
|
|
}
|
|
if diff := cmp.Diff(tt.want, mr1, opts...); diff != "" {
|
|
t.Errorf("wrong result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpgradeNode(t *testing.T) {
|
|
a1 := netip.MustParsePrefix("0.0.0.1/32")
|
|
a2 := netip.MustParsePrefix("0.0.0.2/32")
|
|
a3 := netip.MustParsePrefix("0.0.0.3/32")
|
|
a4 := netip.MustParsePrefix("0.0.0.4/32")
|
|
|
|
tests := []struct {
|
|
name string
|
|
in *tailcfg.Node
|
|
want *tailcfg.Node
|
|
also func(t *testing.T, got *tailcfg.Node) // optional
|
|
}{
|
|
{
|
|
name: "nil",
|
|
in: nil,
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "empty",
|
|
in: new(tailcfg.Node),
|
|
want: new(tailcfg.Node),
|
|
},
|
|
{
|
|
name: "derp-both",
|
|
in: &tailcfg.Node{HomeDERP: 1, LegacyDERPString: tailcfg.DerpMagicIP + ":2"},
|
|
want: &tailcfg.Node{HomeDERP: 1},
|
|
},
|
|
{
|
|
name: "derp-str-only",
|
|
in: &tailcfg.Node{LegacyDERPString: tailcfg.DerpMagicIP + ":2"},
|
|
want: &tailcfg.Node{HomeDERP: 2},
|
|
},
|
|
{
|
|
name: "derp-int-only",
|
|
in: &tailcfg.Node{HomeDERP: 2},
|
|
want: &tailcfg.Node{HomeDERP: 2},
|
|
},
|
|
{
|
|
name: "implicit-allowed-ips-all-set",
|
|
in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a3, a4}},
|
|
want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a3, a4}},
|
|
},
|
|
{
|
|
name: "implicit-allowed-ips-only-address-set",
|
|
in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}},
|
|
want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2}},
|
|
also: func(t *testing.T, got *tailcfg.Node) {
|
|
if t.Failed() {
|
|
return
|
|
}
|
|
if &got.Addresses[0] == &got.AllowedIPs[0] {
|
|
t.Error("Addresses and AllowIPs alias the same memory")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "implicit-allowed-ips-set-empty-slice",
|
|
in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{}},
|
|
want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{}},
|
|
},
|
|
{
|
|
// An unsigned peer is not covered by tailnet lock and must not carry advertised routes
|
|
name: "unsigned-peer-strips-extra-allowed-ips",
|
|
in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2, a3, a4}, UnsignedPeerAPIOnly: true},
|
|
want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2}, UnsignedPeerAPIOnly: true},
|
|
},
|
|
{
|
|
// An unsigned peer whose AllowedIPs already equal its Addresses is left untouched
|
|
name: "unsigned-peer-allowed-ips-equal-addresses",
|
|
in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2}, UnsignedPeerAPIOnly: true},
|
|
want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2}, UnsignedPeerAPIOnly: true},
|
|
},
|
|
{
|
|
// A signed peer keeps its advertised routes: the strip only applies to unsigned peers
|
|
name: "signed-peer-keeps-extra-allowed-ips",
|
|
in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2, a3, a4}},
|
|
want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2, a3, a4}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var got *tailcfg.Node
|
|
if tt.in != nil {
|
|
got = new(*tt.in) // shallow clone
|
|
}
|
|
upgradeNode(got)
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
t.Errorf("wrong result (-want +got):\n%s", diff)
|
|
}
|
|
if tt.also != nil {
|
|
tt.also(t, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkMapSessionDelta(b *testing.B) {
|
|
for _, size := range []int{10, 100, 1_000, 10_000} {
|
|
b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
|
|
ctx := context.Background()
|
|
nu := &countingNetmapUpdater{}
|
|
ms := newTestMapSession(b, nu)
|
|
// Disable log output for benchmarks to avoid races
|
|
ms.logf = func(string, ...any) {}
|
|
res := &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo.bar.ts.net.",
|
|
},
|
|
}
|
|
for i := range size {
|
|
res.Peers = append(res.Peers, &tailcfg.Node{
|
|
ID: tailcfg.NodeID(i + 2),
|
|
Name: fmt.Sprintf("peer%d.bar.ts.net.", i),
|
|
HomeDERP: 10,
|
|
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")},
|
|
AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")},
|
|
Endpoints: eps("192.168.1.2:345", "192.168.1.3:678"),
|
|
Hostinfo: (&tailcfg.Hostinfo{
|
|
OS: "fooOS",
|
|
Hostname: "MyHostname",
|
|
Services: []tailcfg.Service{
|
|
{Proto: "peerapi4", Port: 1234},
|
|
{Proto: "peerapi6", Port: 1234},
|
|
{Proto: "peerapi-dns-proxy", Port: 1},
|
|
},
|
|
}).View(),
|
|
LastSeen: new(time.Unix(int64(i), 0)),
|
|
})
|
|
}
|
|
ms.HandleNonKeepAliveMapResponse(ctx, res)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
// Now for the core of the benchmark loop, just toggle
|
|
// a single node's online status.
|
|
for i := range b.N {
|
|
if err := ms.HandleNonKeepAliveMapResponse(ctx, &tailcfg.MapResponse{
|
|
OnlineChange: map[tailcfg.NodeID]bool{
|
|
2: i%2 == 0,
|
|
},
|
|
}); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNetmapDisplayMessage checks that the various diff operations
|
|
// (add/update/delete/clear) for [tailcfg.DisplayMessage] in a
|
|
// [tailcfg.MapResponse] work as expected.
|
|
func TestNetmapDisplayMessage(t *testing.T) {
|
|
type test struct {
|
|
name string
|
|
initialState *tailcfg.MapResponse
|
|
mapResponse tailcfg.MapResponse
|
|
wantMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
|
}
|
|
|
|
tests := []test{
|
|
{
|
|
name: "basic-set",
|
|
mapResponse: tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"test-message": {
|
|
Title: "Testing",
|
|
Text: "This is a test message",
|
|
Severity: tailcfg.SeverityHigh,
|
|
ImpactsConnectivity: true,
|
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
|
URL: "https://www.example.com",
|
|
Label: "Learn more",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
|
"test-message": {
|
|
Title: "Testing",
|
|
Text: "This is a test message",
|
|
Severity: tailcfg.SeverityHigh,
|
|
ImpactsConnectivity: true,
|
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
|
URL: "https://www.example.com",
|
|
Label: "Learn more",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "delete-one",
|
|
initialState: &tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A",
|
|
},
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
mapResponse: tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": nil,
|
|
},
|
|
},
|
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update-one",
|
|
initialState: &tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A",
|
|
},
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
mapResponse: tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A updated",
|
|
},
|
|
},
|
|
},
|
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A updated",
|
|
},
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "add-one",
|
|
initialState: &tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A",
|
|
},
|
|
},
|
|
},
|
|
mapResponse: tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A",
|
|
},
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "delete-all",
|
|
initialState: &tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A",
|
|
},
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
mapResponse: tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"*": nil,
|
|
},
|
|
},
|
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{},
|
|
},
|
|
{
|
|
name: "delete-all-and-add",
|
|
initialState: &tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"message-a": {
|
|
Title: "Message A",
|
|
},
|
|
"message-b": {
|
|
Title: "Message B",
|
|
},
|
|
},
|
|
},
|
|
mapResponse: tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"*": nil,
|
|
"message-c": {
|
|
Title: "Message C",
|
|
},
|
|
},
|
|
},
|
|
wantMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
|
|
"message-c": {
|
|
Title: "Message C",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ms := newTestMapSession(t, nil)
|
|
|
|
if test.initialState != nil {
|
|
ms.netmapForResponse(test.initialState)
|
|
}
|
|
|
|
nm := ms.netmapForResponse(&test.mapResponse)
|
|
|
|
if diff := cmp.Diff(test.wantMessages, nm.DisplayMessages, cmpopts.EquateEmpty()); diff != "" {
|
|
t.Errorf("unexpected warnings (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNetmapHealthIntegration checks that we get the expected health warnings
|
|
// from processing a [tailcfg.MapResponse] containing health messages and passing the
|
|
// [netmap.NetworkMap] to a [health.Tracker].
|
|
func TestNetmapHealthIntegration(t *testing.T) {
|
|
ms := newTestMapSession(t, nil)
|
|
ht := health.NewTracker(eventbustest.NewBus(t))
|
|
|
|
ht.SetIPNState("NeedsLogin", true)
|
|
ht.GotStreamedMapResponse()
|
|
|
|
nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
Health: []string{
|
|
"Test message",
|
|
"Another message",
|
|
},
|
|
})
|
|
ht.SetControlHealth(nm.DisplayMessages)
|
|
|
|
want := map[health.WarnableCode]health.UnhealthyState{
|
|
"control-health.health-c0719e9a8d5d838d861dc6f675c899d2b309a3a65bb9fe6b11e5afcbf9a2c0b1": {
|
|
WarnableCode: "control-health.health-c0719e9a8d5d838d861dc6f675c899d2b309a3a65bb9fe6b11e5afcbf9a2c0b1",
|
|
Title: "Coordination server reports an issue",
|
|
Severity: health.SeverityMedium,
|
|
Text: "The coordination server is reporting a health issue: Test message",
|
|
},
|
|
"control-health.health-1dc7017a73a3c55c0d6a8423e3813c7ab6562d9d3064c2ec6ac7822f61b1db9c": {
|
|
WarnableCode: "control-health.health-1dc7017a73a3c55c0d6a8423e3813c7ab6562d9d3064c2ec6ac7822f61b1db9c",
|
|
Title: "Coordination server reports an issue",
|
|
Severity: health.SeverityMedium,
|
|
Text: "The coordination server is reporting a health issue: Another message",
|
|
},
|
|
}
|
|
|
|
got := maps.Clone(ht.CurrentState().Warnings)
|
|
for k := range got {
|
|
if !strings.HasPrefix(string(k), "control-health") {
|
|
delete(got, k)
|
|
}
|
|
}
|
|
|
|
if d := cmp.Diff(want, got, cmpopts.IgnoreFields(health.UnhealthyState{}, "ETag")); d != "" {
|
|
t.Fatalf("CurrentStatus().Warnings[\"control-health*\"] different than expected (-want +got)\n%s", d)
|
|
}
|
|
}
|
|
|
|
// TestNetmapDisplayMessageIntegration checks that we get the expected health
|
|
// warnings from processing a [tailcfg.MapResponse] that contains DisplayMessages and
|
|
// passing the [netmap.NetworkMap] to a [health.Tracker].
|
|
func TestNetmapDisplayMessageIntegration(t *testing.T) {
|
|
ms := newTestMapSession(t, nil)
|
|
ht := health.NewTracker(eventbustest.NewBus(t))
|
|
|
|
ht.SetIPNState("NeedsLogin", true)
|
|
ht.GotStreamedMapResponse()
|
|
baseWarnings := ht.CurrentState().Warnings
|
|
|
|
nm := ms.netmapForResponse(&tailcfg.MapResponse{
|
|
DisplayMessages: map[tailcfg.DisplayMessageID]*tailcfg.DisplayMessage{
|
|
"test-message": {
|
|
Title: "Testing",
|
|
Text: "This is a test message",
|
|
Severity: tailcfg.SeverityHigh,
|
|
ImpactsConnectivity: true,
|
|
PrimaryAction: &tailcfg.DisplayMessageAction{
|
|
URL: "https://www.example.com",
|
|
Label: "Learn more",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
ht.SetControlHealth(nm.DisplayMessages)
|
|
|
|
state := ht.CurrentState()
|
|
|
|
// Ignore warnings that aren't from the netmap
|
|
for k := range baseWarnings {
|
|
delete(state.Warnings, k)
|
|
}
|
|
|
|
want := map[health.WarnableCode]health.UnhealthyState{
|
|
"control-health.test-message": {
|
|
WarnableCode: "control-health.test-message",
|
|
Title: "Testing",
|
|
Text: "This is a test message",
|
|
Severity: health.SeverityHigh,
|
|
ImpactsConnectivity: true,
|
|
PrimaryAction: &health.UnhealthyStateAction{
|
|
URL: "https://www.example.com",
|
|
Label: "Learn more",
|
|
},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(want, state.Warnings, cmpopts.IgnoreFields(health.UnhealthyState{}, "ETag")); diff != "" {
|
|
t.Errorf("unexpected message contents (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestNetmapForMapResponseForDebug(t *testing.T) {
|
|
mr := &tailcfg.MapResponse{
|
|
Node: &tailcfg.Node{
|
|
ID: 1,
|
|
Name: "foo.bar.ts.net.",
|
|
},
|
|
Peers: []*tailcfg.Node{
|
|
{ID: 2, Name: "peer1.bar.ts.net.", HomeDERP: 1},
|
|
{ID: 3, Name: "peer2.bar.ts.net.", HomeDERP: 1},
|
|
},
|
|
}
|
|
ms := newTestMapSession(t, nil)
|
|
nm1 := ms.netmapForResponse(mr)
|
|
|
|
prefs := &ipn.Prefs{Persist: &persist.Persist{PrivateNodeKey: ms.privateNodeKey}}
|
|
nm2, err := NetmapFromMapResponseForDebug(t.Context(), prefs.View().Persist(), mr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(nm1, nm2) {
|
|
t.Errorf("mismatch\nnm1: %s\nnm2: %s\n", logger.AsJSON(nm1), logger.AsJSON(nm2))
|
|
}
|
|
}
|
|
|
|
func TestLearnZstdOfKeepAlive(t *testing.T) {
|
|
keepAliveMsgZstd := (func() []byte {
|
|
msg := must.Get(json.Marshal(tailcfg.MapResponse{
|
|
KeepAlive: true,
|
|
}))
|
|
return zstdframe.AppendEncode(nil, msg, zstdframe.FastestCompression)
|
|
})()
|
|
|
|
sess := newTestMapSession(t, nil)
|
|
|
|
// The first time we see a zstd keep-alive message, we learn how
|
|
// the server encodes that.
|
|
var mr tailcfg.MapResponse
|
|
must.Do(sess.decodeMsg(keepAliveMsgZstd, &mr))
|
|
if !mr.KeepAlive {
|
|
t.Fatal("mr.KeepAlive false; want true")
|
|
}
|
|
if !bytes.Equal(sess.keepAliveZ, keepAliveMsgZstd) {
|
|
t.Fatalf("sess.keepAlive = %q; want %q", sess.keepAliveZ, keepAliveMsgZstd)
|
|
}
|
|
if got, want := sess.ztdDecodesForTest, 1; got != want {
|
|
t.Fatalf("got %d zstd decodes; want %d", got, want)
|
|
}
|
|
|
|
// The second time on the session where we see that message, we
|
|
// decode it without needing to decompress.
|
|
var mr2 tailcfg.MapResponse
|
|
must.Do(sess.decodeMsg(keepAliveMsgZstd, &mr2))
|
|
if !mr2.KeepAlive {
|
|
t.Fatal("mr2.KeepAlive false; want true")
|
|
}
|
|
if got, want := sess.ztdDecodesForTest, 1; got != want {
|
|
t.Fatalf("got %d zstd decodes; want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestPeerIDAndKeyByTailscaleIP(t *testing.T) {
|
|
peerKey1 := key.NewNode().Public()
|
|
peerKey2 := key.NewNode().Public()
|
|
|
|
peer1 := &tailcfg.Node{
|
|
ID: 1,
|
|
Key: peerKey1,
|
|
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
|
}
|
|
peer2 := &tailcfg.Node{
|
|
ID: 2,
|
|
Key: peerKey2,
|
|
Addresses: []netip.Prefix{
|
|
netip.MustParsePrefix("100.64.0.2/32"),
|
|
netip.MustParsePrefix("fd7a:115c::2/128"),
|
|
},
|
|
}
|
|
|
|
ms := newTestMapSession(t, nil)
|
|
ms.updateStateFromResponse(&tailcfg.MapResponse{
|
|
Node: new(tailcfg.Node),
|
|
Peers: []*tailcfg.Node{peer1, peer2},
|
|
})
|
|
|
|
t.Run("known_ip_peer1", func(t *testing.T) {
|
|
gotID, gotKey, ok := ms.PeerIDAndKeyByTailscaleIP(netip.MustParseAddr("100.64.0.1"))
|
|
if !ok {
|
|
t.Fatal("PeerIDAndKeyByTailscaleIP returned ok=false, want true")
|
|
}
|
|
if gotID != peer1.ID {
|
|
t.Errorf("NodeID = %v, want %v", gotID, peer1.ID)
|
|
}
|
|
if gotKey != peerKey1 {
|
|
t.Errorf("NodePublic = %v, want %v", gotKey, peerKey1)
|
|
}
|
|
})
|
|
|
|
t.Run("known_ip_peer2_v6", func(t *testing.T) {
|
|
gotID, gotKey, ok := ms.PeerIDAndKeyByTailscaleIP(netip.MustParseAddr("fd7a:115c::2"))
|
|
if !ok {
|
|
t.Fatal("PeerIDAndKeyByTailscaleIP returned ok=false, want true")
|
|
}
|
|
if gotID != peer2.ID {
|
|
t.Errorf("NodeID = %v, want %v", gotID, peer2.ID)
|
|
}
|
|
if gotKey != peerKey2 {
|
|
t.Errorf("NodePublic = %v, want %v", gotKey, peerKey2)
|
|
}
|
|
})
|
|
|
|
t.Run("unknown_ip", func(t *testing.T) {
|
|
gotID, gotKey, ok := ms.PeerIDAndKeyByTailscaleIP(netip.MustParseAddr("100.64.0.99"))
|
|
if ok {
|
|
t.Errorf("PeerIDAndKeyByTailscaleIP returned ok=true for unknown IP, got id=%v key=%v", gotID, gotKey)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRemoveUnwantedDiscoUpdates(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
viaTSMP bool
|
|
existingOnline bool
|
|
sameKey bool
|
|
newerLastSeen bool
|
|
wantAccepted bool
|
|
}{
|
|
{
|
|
name: "tsmp_online_peer_same_key",
|
|
viaTSMP: true,
|
|
existingOnline: true,
|
|
sameKey: true,
|
|
newerLastSeen: true,
|
|
wantAccepted: false,
|
|
},
|
|
{
|
|
name: "not_tsmp_online_peer_same_key",
|
|
viaTSMP: false,
|
|
existingOnline: true,
|
|
sameKey: true,
|
|
newerLastSeen: true,
|
|
wantAccepted: true,
|
|
},
|
|
{
|
|
name: "tsmp_offline_peer_same_key",
|
|
viaTSMP: true,
|
|
existingOnline: false,
|
|
sameKey: true,
|
|
newerLastSeen: true,
|
|
wantAccepted: true,
|
|
},
|
|
{
|
|
name: "tsmp_online_peer_diff_key",
|
|
viaTSMP: true,
|
|
existingOnline: true,
|
|
sameKey: false,
|
|
newerLastSeen: true,
|
|
wantAccepted: true,
|
|
},
|
|
{
|
|
name: "tsmp_online_peer_same_key_old_lastseen",
|
|
viaTSMP: true,
|
|
existingOnline: true,
|
|
sameKey: true,
|
|
newerLastSeen: false,
|
|
wantAccepted: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ms := newTestMapSession(t, &rememberLastNetmapUpdater{done: make(chan any, 1)})
|
|
|
|
existingKey := key.NewDisco().Public()
|
|
existingOnline := tt.existingOnline
|
|
initialLastSeen := time.Unix(1, 0)
|
|
|
|
ms.updateStateFromResponse(&tailcfg.MapResponse{
|
|
Peers: []*tailcfg.Node{{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: existingKey,
|
|
Online: &existingOnline,
|
|
LastSeen: &initialLastSeen,
|
|
}},
|
|
})
|
|
|
|
changeKey := existingKey
|
|
if !tt.sameKey {
|
|
changeKey = key.NewDisco().Public()
|
|
}
|
|
changeOnline := false // must be false to reach the new guard
|
|
updateLastSeen := time.Unix(2, 0)
|
|
if !tt.newerLastSeen {
|
|
updateLastSeen = time.Unix(0, 0)
|
|
}
|
|
|
|
resp := &tailcfg.MapResponse{
|
|
PeersChangedPatch: []*tailcfg.PeerChange{{
|
|
NodeID: 1,
|
|
DiscoKey: &changeKey,
|
|
Online: &changeOnline,
|
|
LastSeen: &updateLastSeen,
|
|
}},
|
|
}
|
|
|
|
ms.removeUnwantedDiscoUpdates(resp, tt.viaTSMP)
|
|
|
|
got := len(resp.PeersChangedPatch) > 0
|
|
if got != tt.wantAccepted {
|
|
t.Errorf("accepted=%v, want %v", got, tt.wantAccepted)
|
|
}
|
|
})
|
|
}
|
|
}
|