| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package controlclient
- import (
- "encoding/json"
- "fmt"
- "reflect"
- "strings"
- "testing"
- "time"
- "go4.org/mem"
- "tailscale.com/tailcfg"
- "tailscale.com/tstest"
- "tailscale.com/types/key"
- "tailscale.com/types/netmap"
- "tailscale.com/types/opt"
- "tailscale.com/types/ptr"
- "tailscale.com/util/must"
- )
- func TestUndeltaPeers(t *testing.T) {
- var curTime time.Time
- tstest.Replace(t, &clockNow, func() time.Time {
- return curTime
- })
- 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 = []string{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
- }{
- {
- name: "full_peers",
- mapRes: &tailcfg.MapResponse{
- Peers: peers(n(1, "foo"), n(2, "bar")),
- },
- want: peers(n(1, "foo"), n(2, "bar")),
- },
- {
- 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")),
- },
- {
- 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")),
- },
- {
- name: "remove",
- prev: peers(n(1, "foo"), n(2, "bar")),
- mapRes: &tailcfg.MapResponse{
- PeersRemoved: []tailcfg.NodeID{1},
- },
- want: peers(n(2, "bar")),
- },
- {
- 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")),
- },
- {
- 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,
- },
- },
- want: peers(
- n(1, "foo", online(true)),
- n(2, "bar"),
- ),
- },
- {
- 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)),
- ),
- },
- {
- 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))),
- ),
- },
- {
- 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"))),
- },
- {
- name: "ep_change_udp",
- prev: peers(n(1, "foo", withEP("1.2.3.4:111"))),
- mapRes: &tailcfg.MapResponse{
- PeersChangedPatch: []*tailcfg.PeerChange{{
- NodeID: 1,
- Endpoints: []string{"1.2.3.4:56"},
- }},
- },
- want: peers(n(1, "foo", withEP("1.2.3.4:56"))),
- },
- {
- name: "ep_change_udp",
- 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: []string{"1.2.3.4:56"},
- }},
- },
- want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))),
- },
- {
- 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: []string{"1.2.3.4:56"},
- }},
- },
- want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))),
- },
- {
- 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'))),
- }),
- },
- {
- 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},
- }),
- },
- {
- 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'))),
- }),
- },
- {
- 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),
- }),
- },
- {
- 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()),
- }),
- },
- {
- 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(),
- }),
- },
- {
- name: "change_capabilities",
- prev: peers(n(1, "foo")),
- mapRes: &tailcfg.MapResponse{
- PeersChangedPatch: []*tailcfg.PeerChange{{
- NodeID: 1,
- Capabilities: ptr.To([]string{"foo"}),
- }},
- }, want: peers(&tailcfg.Node{
- ID: 1,
- Name: "foo",
- Capabilities: []string{"foo"},
- }),
- }}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if !tt.curTime.IsZero() {
- curTime = tt.curTime
- }
- undeltaPeers(tt.mapRes, tt.prev)
- if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
- t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), 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.T) *mapSession {
- ms := newMapSession(key.NewNode())
- ms.logf = t.Logf
- return ms
- }
- 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)
- 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)
- 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)
- 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)
- 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",
- }
- ms := newTestMapSession(t)
- nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
- Node: someNode,
- })
- if nm1.SelfNode == nil {
- 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)
- }
- nm2 := ms.netmapForResponse(&tailcfg.MapResponse{})
- if nm2.SelfNode == nil {
- 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)
- }
- })
- }
- // TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResponses
- // entirely or have their opt.Bool values unspecified between MapResponses in a
- // session and that should mean no change. (as of capver 37). But two Debug
- // fields existed prior to capver 37 that weren't opt.Bool; we test that we both
- // still accept the non-opt.Bool form from control for RandomizeClientPort and
- // ForceBackgroundSTUN and also accept the new form, keeping the old form in
- // sync.
- func TestDeltaDebug(t *testing.T) {
- type step struct {
- got *tailcfg.Debug
- want *tailcfg.Debug
- }
- tests := []struct {
- name string
- steps []step
- }{
- {
- name: "nothing-to-nothing",
- steps: []step{
- {nil, nil},
- {nil, nil},
- },
- },
- {
- name: "sticky-with-old-style-randomize-client-port",
- steps: []step{
- {
- &tailcfg.Debug{RandomizeClientPort: true},
- &tailcfg.Debug{
- RandomizeClientPort: true,
- SetRandomizeClientPort: "true",
- },
- },
- {
- nil, // not sent by server
- &tailcfg.Debug{
- RandomizeClientPort: true,
- SetRandomizeClientPort: "true",
- },
- },
- },
- },
- {
- name: "sticky-with-new-style-randomize-client-port",
- steps: []step{
- {
- &tailcfg.Debug{SetRandomizeClientPort: "true"},
- &tailcfg.Debug{
- RandomizeClientPort: true,
- SetRandomizeClientPort: "true",
- },
- },
- {
- nil, // not sent by server
- &tailcfg.Debug{
- RandomizeClientPort: true,
- SetRandomizeClientPort: "true",
- },
- },
- },
- },
- {
- name: "opt-bool-sticky-changing-over-time",
- steps: []step{
- {nil, nil},
- {nil, nil},
- {
- &tailcfg.Debug{OneCGNATRoute: "true"},
- &tailcfg.Debug{OneCGNATRoute: "true"},
- },
- {
- nil,
- &tailcfg.Debug{OneCGNATRoute: "true"},
- },
- {
- &tailcfg.Debug{OneCGNATRoute: "false"},
- &tailcfg.Debug{OneCGNATRoute: "false"},
- },
- {
- nil,
- &tailcfg.Debug{OneCGNATRoute: "false"},
- },
- },
- },
- {
- name: "legacy-ForceBackgroundSTUN",
- steps: []step{
- {
- &tailcfg.Debug{ForceBackgroundSTUN: true},
- &tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
- },
- },
- },
- {
- name: "opt-bool-SetForceBackgroundSTUN",
- steps: []step{
- {
- &tailcfg.Debug{SetForceBackgroundSTUN: "true"},
- &tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
- },
- },
- },
- {
- name: "server-reset-to-default",
- steps: []step{
- {
- &tailcfg.Debug{SetForceBackgroundSTUN: "true"},
- &tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
- },
- {
- &tailcfg.Debug{SetForceBackgroundSTUN: "unset"},
- &tailcfg.Debug{ForceBackgroundSTUN: false, SetForceBackgroundSTUN: "unset"},
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ms := newTestMapSession(t)
- for stepi, s := range tt.steps {
- nm := ms.netmapForResponse(&tailcfg.MapResponse{Debug: s.got})
- if !reflect.DeepEqual(nm.Debug, s.want) {
- t.Errorf("unexpected result at step index %v; got: %s", stepi, must.Get(json.Marshal(nm.Debug)))
- }
- }
- })
- }
- }
- // Verifies that copyDebugOptBools doesn't missing any opt.Bools.
- func TestCopyDebugOptBools(t *testing.T) {
- rt := reflect.TypeOf(tailcfg.Debug{})
- for i := 0; i < rt.NumField(); i++ {
- sf := rt.Field(i)
- if sf.Type != reflect.TypeOf(opt.Bool("")) {
- continue
- }
- var src, dst tailcfg.Debug
- reflect.ValueOf(&src).Elem().Field(i).Set(reflect.ValueOf(opt.Bool("true")))
- if src == (tailcfg.Debug{}) {
- t.Fatalf("failed to set field %v", sf.Name)
- }
- copyDebugOptBools(&dst, &src)
- if src != dst {
- t.Fatalf("copyDebugOptBools didn't copy field %v", sf.Name)
- }
- }
- }
|