| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106 |
- // 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(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: 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, 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 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: 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, 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) {
- tests := []struct {
- name string
- in *tailcfg.Node
- want *tailcfg.Node
- }{
- {
- 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},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- var got *tailcfg.Node
- if tt.in != nil {
- got = ptr.To(*tt.in) // shallow clone
- }
- upgradeNode(got)
- if diff := cmp.Diff(tt.want, got); 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),
- 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: 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)
- }
- }
- })
- }
- }
|