// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause package wgengine import ( "fmt" "math/rand" "net/netip" "os" "reflect" "runtime" "testing" "go4.org/mem" "tailscale.com/cmd/testwrapper/flakytest" "tailscale.com/control/controlknobs" "tailscale.com/envknob" "tailscale.com/health" "tailscale.com/net/dns" "tailscale.com/net/netaddr" "tailscale.com/net/tstun" "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/tstime/mono" "tailscale.com/types/key" "tailscale.com/types/netmap" "tailscale.com/types/opt" "tailscale.com/types/views" "tailscale.com/util/eventbus/eventbustest" "tailscale.com/util/usermetric" "tailscale.com/wgengine/router" "tailscale.com/wgengine/wgcfg" ) func TestNoteReceiveActivity(t *testing.T) { now := mono.Time(123456) var logBuf tstest.MemLogger confc := make(chan bool, 1) gotConf := func() bool { select { case <-confc: return true default: return false } } e := &userspaceEngine{ timeNow: func() mono.Time { return now }, recvActivityAt: map[key.NodePublic]mono.Time{}, logf: logBuf.Logf, tundev: new(tstun.Wrapper), testMaybeReconfigHook: func() { confc <- true }, trimmedNodes: map[key.NodePublic]bool{}, } ra := e.recvActivityAt nk := key.NewNode().Public() // Activity on an untracked key should do nothing. e.noteRecvActivity(nk) if len(ra) != 0 { t.Fatalf("unexpected growth in map: now has %d keys; want 0", len(ra)) } if logBuf.Len() != 0 { t.Fatalf("unexpected log write (and thus activity): %s", logBuf.Bytes()) } // Now track it, but don't mark it trimmed, so shouldn't update. ra[nk] = 0 e.noteRecvActivity(nk) if len(ra) != 1 { t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra)) } if got := ra[nk]; got != now { t.Fatalf("time in map = %v; want %v", got, now) } if gotConf() { t.Fatalf("unexpected reconfig") } // Now mark it trimmed and expect an update. e.trimmedNodes[nk] = true e.noteRecvActivity(nk) if len(ra) != 1 { t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra)) } if got := ra[nk]; got != now { t.Fatalf("time in map = %v; want %v", got, now) } if !gotConf() { t.Fatalf("didn't get expected reconfig") } } func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { nv := make([]tailcfg.NodeView, len(v)) for i, n := range v { nv[i] = n.View() } return nv } func TestUserspaceEngineReconfig(t *testing.T) { bus := eventbustest.NewBus(t) ht := health.NewTracker(bus) reg := new(usermetric.Registry) e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg, bus) if err != nil { t.Fatal(err) } t.Cleanup(e.Close) ue := e.(*userspaceEngine) routerCfg := &router.Config{} for _, nodeHex := range []string{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } { nm := &netmap.NetworkMap{ Peers: nodeViews([]*tailcfg.Node{ { ID: 1, Key: nkFromHex(nodeHex), }, }), } nk, err := key.ParseNodePublicUntyped(mem.S(nodeHex)) if err != nil { t.Fatal(err) } cfg := &wgcfg.Config{ Peers: []wgcfg.Peer{ { PublicKey: nk, AllowedIPs: []netip.Prefix{ netip.PrefixFrom(netaddr.IPv4(100, 100, 99, 1), 32), }, }, }, } e.SetNetworkMap(nm) err = e.Reconfig(cfg, routerCfg, &dns.Config{}) if err != nil { t.Fatal(err) } wantRecvAt := map[key.NodePublic]mono.Time{ nkFromHex(nodeHex): 0, } if got := ue.recvActivityAt; !reflect.DeepEqual(got, wantRecvAt) { t.Errorf("wrong recvActivityAt\n got: %v\nwant: %v\n", got, wantRecvAt) } wantTrimmedNodes := map[key.NodePublic]bool{ nkFromHex(nodeHex): true, } if got := ue.trimmedNodes; !reflect.DeepEqual(got, wantTrimmedNodes) { t.Errorf("wrong wantTrimmedNodes\n got: %v\nwant: %v\n", got, wantTrimmedNodes) } } } func TestUserspaceEngineTSMPLearned(t *testing.T) { bus := eventbustest.NewBus(t) ht := health.NewTracker(bus) reg := new(usermetric.Registry) e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg, bus) if err != nil { t.Fatal(err) } t.Cleanup(e.Close) ue := e.(*userspaceEngine) discoChangedChan := make(chan map[key.NodePublic]bool, 1) ue.testDiscoChangedHook = func(m map[key.NodePublic]bool) { discoChangedChan <- m } routerCfg := &router.Config{} keyChanges := []struct { tsmp bool inMap bool }{ {tsmp: false, inMap: false}, {tsmp: true, inMap: false}, {tsmp: false, inMap: true}, } nkHex := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" for _, change := range keyChanges { oldDisco := key.NewDisco() nm := &netmap.NetworkMap{ Peers: nodeViews([]*tailcfg.Node{ { ID: 1, Key: nkFromHex(nkHex), DiscoKey: oldDisco.Public(), }, }), } nk, err := key.ParseNodePublicUntyped(mem.S(nkHex)) if err != nil { t.Fatal(err) } e.SetNetworkMap(nm) newDisco := key.NewDisco() cfg := &wgcfg.Config{ Peers: []wgcfg.Peer{ { PublicKey: nk, DiscoKey: newDisco.Public(), }, }, } if change.tsmp { ue.PatchDiscoKey(nk, newDisco.Public()) } err = e.Reconfig(cfg, routerCfg, &dns.Config{}) if err != nil { t.Fatal(err) } changeMap := <-discoChangedChan if _, ok := changeMap[nk]; ok != change.inMap { t.Fatalf("expect key %v in map %v to be %t, got %t", nk, changeMap, change.inMap, ok) } } } func TestUserspaceEngineTSMPLearnedMismatch(t *testing.T) { bus := eventbustest.NewBus(t) ht := health.NewTracker(bus) reg := new(usermetric.Registry) e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg, bus) if err != nil { t.Fatal(err) } t.Cleanup(e.Close) ue := e.(*userspaceEngine) discoChangedChan := make(chan map[key.NodePublic]bool, 1) ue.testDiscoChangedHook = func(m map[key.NodePublic]bool) { discoChangedChan <- m } routerCfg := &router.Config{} var metricValue int64 = 0 keyChanges := []struct { tsmp bool inMap bool wrongKey bool }{ {tsmp: false, inMap: false, wrongKey: false}, {tsmp: true, inMap: false, wrongKey: true}, {tsmp: false, inMap: false, wrongKey: false}, } nkHex := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" for _, change := range keyChanges { oldDisco := key.NewDisco() nm := &netmap.NetworkMap{ Peers: nodeViews([]*tailcfg.Node{ { ID: 1, Key: nkFromHex(nkHex), DiscoKey: oldDisco.Public(), }, }), } nk, err := key.ParseNodePublicUntyped(mem.S(nkHex)) if err != nil { t.Fatal(err) } e.SetNetworkMap(nm) newDisco := key.NewDisco() cfg := &wgcfg.Config{ Peers: []wgcfg.Peer{ { PublicKey: nk, DiscoKey: newDisco.Public(), }, }, } tsmpKey := newDisco.Public() if change.tsmp { if change.wrongKey { tsmpKey = key.NewDisco().Public() } ue.PatchDiscoKey(nk, tsmpKey) } err = e.Reconfig(cfg, routerCfg, &dns.Config{}) if err != nil { t.Fatal(err) } changeMap := <-discoChangedChan if _, ok := changeMap[nk]; ok != change.inMap { t.Fatalf("expect key %v in map %v to be %t, got %t", nk, changeMap, change.inMap, ok) } metric := metricTSMPLearnedKeyMismatch.Value() delta := metric - metricValue metricValue = metric if change.wrongKey && delta != 1 { t.Fatalf("expected a delta of 1, got %d", delta) } } } func TestUserspaceEnginePortReconfig(t *testing.T) { flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/2855") const defaultPort = 49983 var knobs controlknobs.Knobs bus := eventbustest.NewBus(t) // Keep making a wgengine until we find an unused port var ue *userspaceEngine ht := health.NewTracker(bus) reg := new(usermetric.Registry) for range 100 { attempt := uint16(defaultPort + rand.Intn(1000)) e, err := NewFakeUserspaceEngine(t.Logf, attempt, &knobs, ht, reg, bus) if err != nil { t.Fatal(err) } ue = e.(*userspaceEngine) if ue.magicConn.LocalPort() == attempt { break } ue.Close() ue = nil } if ue == nil { t.Fatal("could not create a wgengine with a specific port") } t.Cleanup(ue.Close) startingPort := ue.magicConn.LocalPort() nodeKey, err := key.ParseNodePublicUntyped(mem.S("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) if err != nil { t.Fatal(err) } cfg := &wgcfg.Config{ Peers: []wgcfg.Peer{ { PublicKey: nodeKey, AllowedIPs: []netip.Prefix{ netip.PrefixFrom(netaddr.IPv4(100, 100, 99, 1), 32), }, }, }, } routerCfg := &router.Config{} if err := ue.Reconfig(cfg, routerCfg, &dns.Config{}); err != nil { t.Fatal(err) } if got := ue.magicConn.LocalPort(); got != startingPort { t.Errorf("no debug setting changed local port to %d from %d", got, startingPort) } knobs.RandomizeClientPort.Store(true) if err := ue.Reconfig(cfg, routerCfg, &dns.Config{}); err != nil { t.Fatal(err) } if got := ue.magicConn.LocalPort(); got == startingPort { t.Errorf("debug setting did not change local port from %d", startingPort) } lastPort := ue.magicConn.LocalPort() knobs.RandomizeClientPort.Store(false) if err := ue.Reconfig(cfg, routerCfg, &dns.Config{}); err != nil { t.Fatal(err) } if startingPort == defaultPort { // Only try this if we managed to bind defaultPort the first time. // Otherwise, assume someone else on the computer is using defaultPort // and so Reconfig would have caused magicSockt to bind some other port. if got := ue.magicConn.LocalPort(); got != defaultPort { t.Errorf("debug setting did not change local port from %d to %d", startingPort, defaultPort) } } if got := ue.magicConn.LocalPort(); got == lastPort { t.Errorf("Reconfig did not change local port from %d", lastPort) } } // Test that enabling and disabling peer path MTU discovery works correctly. func TestUserspaceEnginePeerMTUReconfig(t *testing.T) { if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skipf("skipping on %q; peer MTU not supported", runtime.GOOS) } defer os.Setenv("TS_DEBUG_ENABLE_PMTUD", os.Getenv("TS_DEBUG_ENABLE_PMTUD")) envknob.Setenv("TS_DEBUG_ENABLE_PMTUD", "") // Turn on debugging to help diagnose problems. defer os.Setenv("TS_DEBUG_PMTUD", os.Getenv("TS_DEBUG_PMTUD")) envknob.Setenv("TS_DEBUG_PMTUD", "true") var knobs controlknobs.Knobs bus := eventbustest.NewBus(t) ht := health.NewTracker(bus) reg := new(usermetric.Registry) e, err := NewFakeUserspaceEngine(t.Logf, 0, &knobs, ht, reg, bus) if err != nil { t.Fatal(err) } t.Cleanup(e.Close) ue := e.(*userspaceEngine) if ue.magicConn.PeerMTUEnabled() != false { t.Error("peer MTU enabled by default, should not be") } osDefaultDF, err := ue.magicConn.DontFragSetting() if err != nil { t.Errorf("get don't fragment bit failed: %v", err) } t.Logf("Info: OS default don't fragment bit(s) setting: %v", osDefaultDF) // Build a set of configs to use as we change the peer MTU settings. nodeKey, err := key.ParseNodePublicUntyped(mem.S("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) if err != nil { t.Fatal(err) } cfg := &wgcfg.Config{ Peers: []wgcfg.Peer{ { PublicKey: nodeKey, AllowedIPs: []netip.Prefix{ netip.PrefixFrom(netaddr.IPv4(100, 100, 99, 1), 32), }, }, }, } routerCfg := &router.Config{} tests := []struct { desc string // test description wantP bool // desired value of PMTUD setting wantDF bool // desired value of don't fragment bits shouldP opt.Bool // if set, force peer MTU to this value }{ {desc: "after_first_reconfig", wantP: false, wantDF: osDefaultDF, shouldP: ""}, {desc: "enabling_PMTUD_first_time", wantP: true, wantDF: true, shouldP: "true"}, {desc: "disabling_PMTUD", wantP: false, wantDF: false, shouldP: "false"}, {desc: "enabling_PMTUD_second_time", wantP: true, wantDF: true, shouldP: "true"}, {desc: "returning_to_default_PMTUD", wantP: false, wantDF: false, shouldP: ""}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { if v, ok := tt.shouldP.Get(); ok { knobs.PeerMTUEnable.Store(v) } else { knobs.PeerMTUEnable.Store(false) } if err := ue.Reconfig(cfg, routerCfg, &dns.Config{}); err != nil { t.Fatal(err) } if v := ue.magicConn.PeerMTUEnabled(); v != tt.wantP { t.Errorf("peer MTU set to %v, want %v", v, tt.wantP) } if v, err := ue.magicConn.DontFragSetting(); v != tt.wantDF || err != nil { t.Errorf("don't fragment bit set to %v, want %v, err %v", v, tt.wantP, err) } }) } } func TestTSMPKeyAdvertisement(t *testing.T) { var knobs controlknobs.Knobs bus := eventbustest.NewBus(t) ht := health.NewTracker(bus) reg := new(usermetric.Registry) e, err := NewFakeUserspaceEngine(t.Logf, 0, &knobs, ht, reg, bus) if err != nil { t.Fatal(err) } t.Cleanup(e.Close) ue := e.(*userspaceEngine) routerCfg := &router.Config{} nodeKey := nkFromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") nm := &netmap.NetworkMap{ Peers: nodeViews([]*tailcfg.Node{ { ID: 1, Key: nodeKey, }, }), SelfNode: (&tailcfg.Node{ StableID: "TESTCTRL00000001", Name: "test-node.test.ts.net", Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("fd7a:115c:a1e0:ab12:4843:cd96:0:1/128")}, }).View(), } cfg := &wgcfg.Config{ Peers: []wgcfg.Peer{ { PublicKey: nodeKey, AllowedIPs: []netip.Prefix{ netip.PrefixFrom(netaddr.IPv4(100, 100, 99, 1), 32), }, }, }, } ue.SetNetworkMap(nm) err = ue.Reconfig(cfg, routerCfg, &dns.Config{}) if err != nil { t.Fatal(err) } addr := netip.MustParseAddr("100.100.99.1") previousValue := metricTSMPDiscoKeyAdvertisementSent.Value() ue.sendTSMPDiscoAdvertisement(addr) if val := metricTSMPDiscoKeyAdvertisementSent.Value(); val <= previousValue { errs := metricTSMPDiscoKeyAdvertisementError.Value() t.Errorf("Expected 1 disco key advert, got %d, errors %d", val, errs) } // Remove config to have the engine shut down more consistently err = ue.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{}) if err != nil { t.Fatal(err) } } func nkFromHex(hex string) key.NodePublic { if len(hex) != 64 { panic(fmt.Sprintf("%q is len %d; want 64", hex, len(hex))) } k, err := key.ParseNodePublicUntyped(mem.S(hex[:64])) if err != nil { panic(fmt.Sprintf("%q is not hex: %v", hex, err)) } return k } // makeMaybeReconfigInputs builds a maybeReconfigInputs with n peers, // each with a unique key, disco key, and AllowedIPs entry. func makeMaybeReconfigInputs(n int) *maybeReconfigInputs { peers := make([]wgcfg.Peer, n) trimmed := make(map[key.NodePublic]bool, n) trackNodes := make([]key.NodePublic, n) trackIPs := make([]netip.Addr, n) for i := range n { nk := key.NewNode() pub := nk.Public() peers[i] = wgcfg.Peer{ PublicKey: pub, DiscoKey: key.NewDisco().Public(), AllowedIPs: []netip.Prefix{netip.PrefixFrom(netip.AddrFrom4([4]byte{100, 64, byte(i >> 8), byte(i)}), 32)}, } trimmed[pub] = true trackNodes[i] = pub trackIPs[i] = netip.AddrFrom4([4]byte{100, 64, byte(i >> 8), byte(i)}) } return &maybeReconfigInputs{ WGConfig: &wgcfg.Config{ PrivateKey: key.NewNode(), Peers: peers, MTU: 1280, }, TrimmedNodes: trimmed, TrackNodes: views.SliceOf(trackNodes), TrackIPs: views.SliceOf(trackIPs), } } func TestMaybeReconfigInputsEqual(t *testing.T) { a := makeMaybeReconfigInputs(100) b := a.Clone() // nil cases if !(*maybeReconfigInputs)(nil).Equal(nil) { t.Error("nil.Equal(nil) should be true") } if a.Equal(nil) { t.Error("non-nil.Equal(nil) should be false") } if (*maybeReconfigInputs)(nil).Equal(a) { t.Error("nil.Equal(non-nil) should be false") } // same pointer if !a.Equal(a) { t.Error("a.Equal(a) should be true") } // cloned equal value if !a.Equal(b) { t.Error("a.Equal(clone) should be true") } // Verify that every field in the struct is covered by Equal. // Each entry mutates exactly one field of a clone and expects // Equal to return false. If a new field is added to // maybeReconfigInputs without a corresponding entry here, the // field count check below will fail. type mutator struct { field string fn func(c *maybeReconfigInputs) } mutators := []mutator{ {"WGConfig", func(c *maybeReconfigInputs) { c.WGConfig.MTU = 9999 }}, {"TrimmedNodes", func(c *maybeReconfigInputs) { c.TrimmedNodes[key.NewNode().Public()] = true }}, {"TrackNodes", func(c *maybeReconfigInputs) { ns := c.TrackNodes.AsSlice() ns[0] = key.NewNode().Public() c.TrackNodes = views.SliceOf(ns) }}, {"TrackIPs", func(c *maybeReconfigInputs) { ips := c.TrackIPs.AsSlice() ips[0] = netip.MustParseAddr("1.2.3.4") c.TrackIPs = views.SliceOf(ips) }}, } // Ensure we have a mutator for every field. numFields := reflect.TypeOf(maybeReconfigInputs{}).NumField() if len(mutators) != numFields { t.Fatalf("maybeReconfigInputs has %d fields but test covers %d; update the mutators table", numFields, len(mutators)) } for _, m := range mutators { c := a.Clone() m.fn(c) if a.Equal(c) { t.Errorf("Equal did not detect change in field %s", m.field) } } } func BenchmarkMaybeReconfigInputsEqual(b *testing.B) { for _, n := range []int{10, 100, 1000, 5000} { b.Run(fmt.Sprintf("peers=%d", n), func(b *testing.B) { a := makeMaybeReconfigInputs(n) o := a.Clone() b.ReportAllocs() b.ResetTimer() for range b.N { a.Equal(o) } }) } } // an experiment to see if genLocalAddrFunc was worth it. As of Go // 1.16, it still very much is. (30-40x faster) func BenchmarkGenLocalAddrFunc(b *testing.B) { la1 := netip.MustParseAddr("1.2.3.4") la2 := netip.MustParseAddr("::4") lanot := netip.MustParseAddr("5.5.5.5") var x bool b.Run("map1", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() m := map[netip.Addr]bool{ la1: true, } for range b.N { x = m[la1] x = m[lanot] } }) b.Run("map2", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() m := map[netip.Addr]bool{ la1: true, la2: true, } for range b.N { x = m[la1] x = m[lanot] } }) b.Run("or1", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() f := func(t netip.Addr) bool { return t == la1 } for range b.N { x = f(la1) x = f(lanot) } }) b.Run("or2", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() f := func(t netip.Addr) bool { return t == la1 || t == la2 } for range b.N { x = f(la1) x = f(lanot) } }) b.Logf("x = %v", x) }