diff options
Diffstat (limited to 'tstest/controll/controll.go')
| -rw-r--r-- | tstest/controll/controll.go | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/tstest/controll/controll.go b/tstest/controll/controll.go new file mode 100644 index 000000000..9afb1979f --- /dev/null +++ b/tstest/controll/controll.go @@ -0,0 +1,290 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// The controll program trolls tailscaleds, simulating huge and busy tailnets. +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "math/rand/v2" + "net/http" + "net/netip" + "os" + "path/filepath" + "testing" + "time" + + "golang.org/x/crypto/acme/autocert" + "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" + "tailscale.com/tstest/integration" + "tailscale.com/tstest/integration/testcontrol" + "tailscale.com/types/key" + "tailscale.com/types/logger" + "tailscale.com/types/ptr" + "tailscale.com/util/must" +) + +var ( + flagNFake = flag.Int("nfake", 0, "number of fake nodes to add to network") + certHost = flag.String("certhost", "controll.fitz.dev", "hostname to use in TLS certificate") +) + +type state struct { + Legacy key.ControlPrivate + Machine key.MachinePrivate +} + +func loadState() *state { + st := &state{} + path := filepath.Join(must.Get(os.UserCacheDir()), "controll.state") + f, _ := os.ReadFile(path) + f = bytes.TrimSpace(f) + if err := json.Unmarshal(f, st); err == nil { + return st + } + st.Legacy = key.NewControl() + st.Machine = key.NewMachine() + f = must.Get(json.Marshal(st)) + must.Do(os.WriteFile(path, f, 0600)) + return st +} + +func main() { + flag.Parse() + + var t fakeTB + derpMap := integration.RunDERPAndSTUN(t, logger.Discard, "127.0.0.1") + + certManager := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(*certHost), + Cache: autocert.DirCache(filepath.Join(must.Get(os.UserCacheDir()), "controll-cert")), + } + + control := &testcontrol.Server{ + DERPMap: derpMap, + ExplicitBaseURL: "http://127.0.0.1:9911", + TolerateUnknownPaths: true, + AltMapStream: sendClientChaos, + } + + st := loadState() + control.SetPrivateKeys(st.Machine, st.Legacy) + for range *flagNFake { + control.AddFakeNode() + } + mux := http.NewServeMux() + mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("<html><body>con<b>troll</b>")) + }) + mux.Handle("/", control) + + go func() { + addr := "127.0.0.1:9911" + log.Printf("listening on %s", addr) + err := http.ListenAndServe(addr, mux) + log.Fatal(err) + }() + + if *certHost != "" { + go func() { + srv := &http.Server{ + Addr: ":https", + Handler: mux, + TLSConfig: certManager.TLSConfig(), + } + log.Fatalf("TLS: %v", srv.ListenAndServeTLS("", "")) + }() + } + + select {} +} + +func node4(nid tailcfg.NodeID) netip.Prefix { + return netip.PrefixFrom( + netip.AddrFrom4([4]byte{100, 100 + byte(nid>>16), byte(nid >> 8), byte(nid)}), + 32) +} + +func node6(nid tailcfg.NodeID) netip.Prefix { + a := tsaddr.TailscaleULARange().Addr().As16() + a[13] = byte(nid >> 16) + a[14] = byte(nid >> 8) + a[15] = byte(nid) + v6 := netip.AddrFrom16(a) + return netip.PrefixFrom(v6, 128) +} + +func sendClientChaos(ctx context.Context, w testcontrol.MapStreamWriter, r *tailcfg.MapRequest) { + selfPub := r.NodeKey + + nodeID := tailcfg.NodeID(0) + newNodeID := func() tailcfg.NodeID { + nodeID++ + return nodeID + } + + selfNodeID := newNodeID() + selfIP4 := node4(nodeID) + selfIP6 := node6(nodeID) + + selfUserID := tailcfg.UserID(1_000_000) + + var peers []*tailcfg.Node + for range *flagNFake { + nid := newNodeID() + v4, v6 := node4(nid), node6(nid) + user := selfUserID + if rand.IntN(2) == 0 { + // Randomly assign a different user to the peer. + // ... + } + peers = append(peers, &tailcfg.Node{ + ID: nid, + StableID: tailcfg.StableNodeID(fmt.Sprintf("peer-%d", nid)), + Name: fmt.Sprintf("peer-%d.troll.ts.net.", nid), + Key: key.NewNode().Public(), + MachineAuthorized: true, + DiscoKey: key.NewDisco().Public(), + Addresses: []netip.Prefix{v4, v6}, + AllowedIPs: []netip.Prefix{v4, v6}, + User: user, + }) + } + + w.SendMapMessage(&tailcfg.MapResponse{ + Node: &tailcfg.Node{ + ID: selfNodeID, + StableID: "self", + Name: "test-mctestfast.troll.ts.net.", + User: selfUserID, + Key: selfPub, + KeyExpiry: time.Now().Add(5000 * time.Hour), + Machine: key.NewMachine().Public(), // fake; client shouldn't care + DiscoKey: r.DiscoKey, + MachineAuthorized: true, + Addresses: []netip.Prefix{selfIP4, selfIP6}, + AllowedIPs: []netip.Prefix{selfIP4, selfIP6}, + Capabilities: []tailcfg.NodeCapability{}, + CapMap: map[tailcfg.NodeCapability][]tailcfg.RawMessage{}, + }, + DERPMap: &tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 1: {RegionID: 1, + Nodes: []*tailcfg.DERPNode{{ + RegionID: 1, + Name: "1i", + IPv4: "199.38.181.103", + IPv6: "2607:f740:f::e19", + HostName: "derp1i.tailscale.com", + CanPort80: true, + }}}, + }, + }, + Peers: peers, + }) + + sendChange := func() error { + const ( + actionToggleOnline = iota + numActions + ) + action := rand.IntN(numActions) + switch action { + case actionToggleOnline: + peer := peers[rand.IntN(len(peers))] + online := peer.Online != nil && *peer.Online + peer.Online = ptr.To(!online) + var lastSeen *time.Time + if !online { + lastSeen = ptr.To(time.Now().UTC().Round(time.Second)) + } + w.SendMapMessage(&tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{ + { + NodeID: peer.ID, + Online: peer.Online, + LastSeen: lastSeen, + }, + }, + }) + } + return nil + } + + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := sendChange(); err != nil { + log.Printf("sendChange: %v", err) + return + } + case <-ctx.Done(): + return + } + } +} + +type fakeTB struct { + *testing.T +} + +func (t fakeTB) Cleanup(_ func()) {} +func (t fakeTB) Error(args ...any) { + t.Fatal(args...) +} +func (t fakeTB) Errorf(format string, args ...any) { + t.Fatalf(format, args...) +} +func (t fakeTB) Fail() { + t.Fatal("failed") +} +func (t fakeTB) FailNow() { + t.Fatal("failed") +} +func (t fakeTB) Failed() bool { + return false +} +func (t fakeTB) Fatal(args ...any) { + log.Fatal(args...) +} +func (t fakeTB) Fatalf(format string, args ...any) { + log.Fatalf(format, args...) +} +func (t fakeTB) Helper() {} +func (t fakeTB) Log(args ...any) { + log.Print(args...) +} +func (t fakeTB) Logf(format string, args ...any) { + log.Printf(format, args...) +} +func (t fakeTB) Name() string { + return "faketest" +} +func (t fakeTB) Setenv(key string, value string) { + panic("not implemented") +} +func (t fakeTB) Skip(args ...any) { + t.Fatal("skipped") +} +func (t fakeTB) SkipNow() { + t.Fatal("skipnow") +} +func (t fakeTB) Skipf(format string, args ...any) { + t.Logf(format, args...) + t.Fatal("skipped") +} +func (t fakeTB) Skipped() bool { + return false +} +func (t fakeTB) TempDir() string { + panic("not implemented") +} |
