mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 10:11:18 +01:00 
			
		
		
		
	Updates #11058 Change-Id: I35e7ef9b90e83cac04ca93fd964ad00ed5b48430 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			1061 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1061 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) Tailscale Inc & AUTHORS
 | 
						|
// SPDX-License-Identifier: BSD-3-Clause
 | 
						|
 | 
						|
package controlclient
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"net/netip"
 | 
						|
	"reflect"
 | 
						|
	"strings"
 | 
						|
	"sync/atomic"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/google/go-cmp/cmp"
 | 
						|
	"go4.org/mem"
 | 
						|
	"tailscale.com/control/controlknobs"
 | 
						|
	"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/ptr"
 | 
						|
	"tailscale.com/util/mak"
 | 
						|
	"tailscale.com/util/must"
 | 
						|
)
 | 
						|
 | 
						|
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(d string) func(*tailcfg.Node) {
 | 
						|
		return func(n *tailcfg.Node) {
 | 
						|
			n.DERP = d
 | 
						|
		}
 | 
						|
	}
 | 
						|
	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("127.3.3.40:3"))),
 | 
						|
			mapRes: &tailcfg.MapResponse{
 | 
						|
				PeersChangedPatch: []*tailcfg.PeerChange{{
 | 
						|
					NodeID:     1,
 | 
						|
					DERPRegion: 4,
 | 
						|
				}},
 | 
						|
			},
 | 
						|
			want:      peers(n(1, "foo", withDERP("127.3.3.40: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("127.3.3.40: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("127.3.3.40:3"), withEP("1.2.3.4:56"))),
 | 
						|
			wantStats: updateStats{changed: 1},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "ep_change_both",
 | 
						|
			prev: peers(n(1, "foo", withDERP("127.3.3.40: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("127.3.3.40: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:    ptr.To(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: ptr.To(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: ptr.To(true),
 | 
						|
				}},
 | 
						|
			},
 | 
						|
			want: peers(&tailcfg.Node{
 | 
						|
				ID:     1,
 | 
						|
				Name:   "foo",
 | 
						|
				Online: ptr.To(true),
 | 
						|
			}),
 | 
						|
			wantStats: updateStats{changed: 1},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "change_last_seen",
 | 
						|
			prev: peers(n(1, "foo")),
 | 
						|
			mapRes: &tailcfg.MapResponse{
 | 
						|
				PeersChangedPatch: []*tailcfg.PeerChange{{
 | 
						|
					NodeID:   1,
 | 
						|
					LastSeen: ptr.To(time.Unix(123, 0).UTC()),
 | 
						|
				}},
 | 
						|
			},
 | 
						|
			want: peers(&tailcfg.Node{
 | 
						|
				ID:       1,
 | 
						|
				Name:     "foo",
 | 
						|
				LastSeen: ptr.To(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: ptr.To(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, 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))
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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 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, DERP: "127.3.3.40:1"},
 | 
						|
			b:    &tailcfg.Node{ID: 1, DERP: "127.3.3.40: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: ptr.To(time.Unix(1, 0))},
 | 
						|
			b:    &tailcfg.Node{ID: 1, LastSeen: ptr.To(time.Unix(2, 0))},
 | 
						|
			want: &tailcfg.PeerChange{NodeID: 1, LastSeen: ptr.To(time.Unix(2, 0))},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "patch-online-to-true",
 | 
						|
			a:    &tailcfg.Node{ID: 1, Online: ptr.To(false)},
 | 
						|
			b:    &tailcfg.Node{ID: 1, Online: ptr.To(true)},
 | 
						|
			want: &tailcfg.PeerChange{NodeID: 1, Online: ptr.To(true)},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "patch-online-to-false",
 | 
						|
			a:    &tailcfg.Node{ID: 1, Online: ptr.To(true)},
 | 
						|
			b:    &tailcfg.Node{ID: 1, Online: ptr.To(false)},
 | 
						|
			want: &tailcfg.PeerChange{NodeID: 1, Online: ptr.To(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: ptr.To(netip.MustParseAddr("100.64.0.1"))},
 | 
						|
			b:    &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.2"))},
 | 
						|
			want: nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "miss-change-masq-v6",
 | 
						|
			a:    &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))},
 | 
						|
			b:    &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: ptr.To(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)
 | 
						|
			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)
 | 
						|
		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)
 | 
						|
}
 | 
						|
 | 
						|
// 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, DERP: "127.3.3.40:1", Hostinfo: hi},
 | 
						|
					{ID: 2, DERP: "127.3.3.40:2", Hostinfo: hi},
 | 
						|
					{ID: 3, DERP: "127.3.3.40:3", Hostinfo: hi},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			mr1: &tailcfg.MapResponse{
 | 
						|
				PeersChanged: []*tailcfg.Node{
 | 
						|
					{ID: 1, DERP: "127.3.3.40:11", Hostinfo: hi},
 | 
						|
					{ID: 2, StableID: "other-change", Hostinfo: hi},
 | 
						|
					{ID: 3, DERP: "127.3.3.40:33", Hostinfo: hi},
 | 
						|
					{ID: 4, DERP: "127.3.3.40:4", Hostinfo: hi},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			want: &tailcfg.MapResponse{
 | 
						|
				PeersChanged: []*tailcfg.Node{
 | 
						|
					{ID: 2, StableID: "other-change", Hostinfo: hi},
 | 
						|
					{ID: 4, DERP: "127.3.3.40: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 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)
 | 
						|
			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),
 | 
						|
					DERP:       "127.3.3.40: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: ptr.To(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)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |