From b37da03e884c49c083685ba2abed7aab98ec75e7 Mon Sep 17 00:00:00 2001 From: julianknodt Date: Mon, 19 Jul 2021 14:28:12 -0700 Subject: [PATCH] tstest/integration: taildrop integration test Adds an integration test for taildrop, testing that taildrop should work in both directions and that the files are identical to the original. Signed-off-by: julianknodt --- tstest/integration/integration_test.go | 95 +++++++++++++++++++ tstest/integration/testcontrol/testcontrol.go | 43 +++++---- tstest/integration/vms/harness_test.go | 7 +- tstest/integration/vms/vm_setup_test.go | 33 +++++++ tstest/integration/vms/vms_test.go | 64 +++++++++++++ 5 files changed, 222 insertions(+), 20 deletions(-) diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index 39c391fb4..de676b8cf 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -20,6 +20,7 @@ "net/http/httptest" "os" "os/exec" + "path" "path/filepath" "regexp" "runtime" @@ -311,6 +312,100 @@ func TestAddPingRequest(t *testing.T) { t.Error("all ping attempts failed") } +func TestTaildrop(t *testing.T) { + // TODO: currently taildrop doesn't work with userspace networking + // but when it does, this test should just work. + t.Skip() + + t.Parallel() + bins := BuildTestBinaries(t) + + env := newTestEnv(t, bins, configureControl(func(control *testcontrol.Server) { + control.AllNodesSameUser = true + })) + defer env.Close() + + n1 := newTestNode(t, env) + n1SocksAddrCh := n1.socks5AddrChan() + d1 := n1.StartDaemon(t) + defer d1.Kill() + + n2 := newTestNode(t, env) + n2SocksAddrCh := n2.socks5AddrChan() + d2 := n2.StartDaemon(t) + defer d2.Kill() + + n1Socks := n1.AwaitSocksAddr(t, n1SocksAddrCh) + n2Socks := n1.AwaitSocksAddr(t, n2SocksAddrCh) + t.Logf("node1 SOCKS5 addr: %v", n1Socks) + t.Logf("node2 SOCKS5 addr: %v", n2Socks) + + for _, n := range env.Control.AllNodes() { + n.Capabilities = append(n.Capabilities, tailcfg.CapabilityFileSharing) + } + + n1.AwaitListening(t) + n2.AwaitListening(t) + n1.MustUp() + n2.MustUp() + n1.AwaitRunning(t) + n2.AwaitRunning(t) + + target := n2.AwaitIP(t) + + srcDir := t.TempDir() + dstDir := t.TempDir() + + fileName := "taildrop.txt" + filePath := path.Join(srcDir, fileName) + contents := []byte("Taildrop drop bop 💧 ??%@#@123˙©∆∆˚ 水平線") + if err := ioutil.WriteFile(filePath, contents, 0666); err != nil { + t.Errorf("Failed to write to file: %v", err) + } + + targetsOutput, err := n1.Tailscale("file", "cp", "-targets").CombinedOutput() + if err != nil { + t.Fatal(string(targetsOutput), err) + } + if !bytes.Contains(targetsOutput, []byte(target.String())) { + t.Errorf("Missing target from cp -targets, want: %v, in: %v", target, targetsOutput) + } + + cpCmd := n1.Tailscale("file", "cp", "-proxy", "socks5://"+n1Socks, filePath, fmt.Sprintf("%s:", target)) + out, err := cpCmd.CombinedOutput() + if err != nil { + t.Fatal(string(out), err) + } + + getCmd := n2.Tailscale("file", "get", dstDir) + if output, err := getCmd.CombinedOutput(); err != nil { + t.Fatal(string(output), err) + } + + files, err := ioutil.ReadDir(dstDir) + if err != nil { + t.Error(err) + } + if len(files) != 1 { + t.Fatalf("want 1 file, got %d", len(files)) + } + + gotFile := files[0] + if !strings.Contains(fileName, gotFile.Name()) { + t.Errorf("want file name %s, got %s", fileName, gotFile.Name()) + } + got, err := ioutil.ReadFile(path.Join(dstDir, gotFile.Name())) + if err != nil { + t.Errorf("Failed to read from cp'd file: %v", err) + } + if !bytes.Equal(got, contents) { + t.Errorf("mismatched taildrop contents, want %s, got %s", contents, got) + } + + d1.MustCleanShutdown(t) + d2.MustCleanShutdown(t) +} + // Issue 2434: when "down" (WantRunning false), tailscaled shouldn't // be connected to control. func TestNoControlConnectionWhenDown(t *testing.T) { diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 7f6792f3d..a845040df 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -41,7 +41,10 @@ type Server struct { Logf logger.Logf // nil means to use the log package DERPMap *tailcfg.DERPMap // nil means to use prod DERP map RequireAuth bool - Verbose bool + // AllNodesSameUser will start all nodes with the same user, + // and is set while testing taildrop which requires nodes with the same user. + AllNodesSameUser bool + Verbose bool // ExplicitBaseURL or HTTPTestServer must be set. ExplicitBaseURL string // e.g. "http://127.0.0.1:1234" with no trailing URL @@ -269,6 +272,7 @@ func (s *Server) nodeLocked(nodeKey tailcfg.NodeKey) *tailcfg.Node { return s.nodes[nodeKey].Clone() } +// AllNodes returns the set of all nodes that are currently active on this server. func (s *Server) AllNodes() (nodes []*tailcfg.Node) { s.mu.Lock() defer s.mu.Unlock() @@ -293,12 +297,16 @@ func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login if u, ok := s.users[nodeKey]; ok { return u, s.logins[nodeKey] } - id := tailcfg.UserID(len(s.users) + 1) + userID := tailcfg.UserID(len(s.users) + 1) + if s.AllNodesSameUser { + userID = 42 + } + nodeID := tailcfg.NodeID(len(s.nodes) + 1) domain := "fake-control.example.net" - loginName := fmt.Sprintf("user-%d@%s", id, domain) - displayName := fmt.Sprintf("User %d", id) + loginName := fmt.Sprintf("user-%d@%s", userID, domain) + displayName := fmt.Sprintf("User %d", userID) login := &tailcfg.Login{ - ID: tailcfg.LoginID(id), + ID: tailcfg.LoginID(nodeID), Provider: "testcontrol", LoginName: loginName, DisplayName: displayName, @@ -306,7 +314,7 @@ func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login Domain: domain, } user := &tailcfg.User{ - ID: id, + ID: userID, LoginName: loginName, DisplayName: displayName, Domain: domain, @@ -408,23 +416,22 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tail machineAuthorized := true // TODO: add Server.RequireMachineAuth - v4Prefix := netaddr.IPPrefixFrom(netaddr.IPv4(100, 64, uint8(tailcfg.NodeID(user.ID)>>8), uint8(tailcfg.NodeID(user.ID))), 32) + v4Prefix := netaddr.IPPrefixFrom(netaddr.IPv4(100, 64, uint8(login.ID>>8), uint8(login.ID)), 32) v6Prefix := netaddr.IPPrefixFrom(tsaddr.Tailscale4To6(v4Prefix.IP()), 128) - allowedIPs := []netaddr.IPPrefix{ - v4Prefix, - v6Prefix, - } + allowedIPs := []netaddr.IPPrefix{v4Prefix, v6Prefix} s.nodes[req.NodeKey] = &tailcfg.Node{ ID: tailcfg.NodeID(user.ID), - StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(user.ID))), + StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(login.ID))), User: user.ID, Machine: mkey, Key: req.NodeKey, MachineAuthorized: machineAuthorized, Addresses: allowedIPs, AllowedIPs: allowedIPs, + Capabilities: []string{"https://tailscale.com/cap/file-sharing"}, + Hostinfo: *req.Hostinfo, } requireAuth := s.RequireAuth if requireAuth && s.nodeKeyAuthed[req.NodeKey] { @@ -536,6 +543,9 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.M jitter := time.Duration(rand.Intn(8000)) * time.Millisecond keepAlive := 50*time.Second + jitter + s.mu.Lock() + s.nodes[req.NodeKey].Hostinfo = *req.Hostinfo + s.mu.Unlock() node := s.Node(req.NodeKey) if node == nil { http.Error(w, "node not found", 400) @@ -645,7 +655,7 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, // node key rotated away (once test server supports that) return nil, nil } - user, _ := s.getUser(req.NodeKey) + user, login := s.getUser(req.NodeKey) res = &tailcfg.MapResponse{ Node: node, DERPMap: s.DERPMap, @@ -662,13 +672,10 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, } } - v4Prefix := netaddr.IPPrefixFrom(netaddr.IPv4(100, 64, uint8(tailcfg.NodeID(user.ID)>>8), uint8(tailcfg.NodeID(user.ID))), 32) + v4Prefix := netaddr.IPPrefixFrom(netaddr.IPv4(100, 64, uint8(login.ID>>8), uint8(login.ID)), 32) v6Prefix := netaddr.IPPrefixFrom(tsaddr.Tailscale4To6(v4Prefix.IP()), 128) - res.Node.Addresses = []netaddr.IPPrefix{ - v4Prefix, - v6Prefix, - } + res.Node.Addresses = []netaddr.IPPrefix{v4Prefix, v6Prefix} res.Node.AllowedIPs = res.Node.Addresses // Consume the PingRequest while protected by mutex if it exists diff --git a/tstest/integration/vms/harness_test.go b/tstest/integration/vms/harness_test.go index ea5a7bba0..faa5b5d09 100644 --- a/tstest/integration/vms/harness_test.go +++ b/tstest/integration/vms/harness_test.go @@ -54,7 +54,10 @@ func newHarness(t *testing.T) *Harness { }) t.Logf("host:port: %s", ln.Addr()) - cs := &testcontrol.Server{} + cs := &testcontrol.Server{ + // TODO should this be set for all tests? + AllNodesSameUser: true, + } derpMap := integration.RunDERPAndSTUN(t, t.Logf, bindHost) cs.DERPMap = derpMap @@ -139,7 +142,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte { cmd := exec.Command(h.bins.CLI, args...) out, err := cmd.CombinedOutput() if err != nil { - t.Fatal(err) + t.Fatalf("cmd %v failed: %v, out: %s", args, err, out) } return out diff --git a/tstest/integration/vms/vm_setup_test.go b/tstest/integration/vms/vm_setup_test.go index d5aa0f1a1..d19ba1e27 100644 --- a/tstest/integration/vms/vm_setup_test.go +++ b/tstest/integration/vms/vm_setup_test.go @@ -303,6 +303,7 @@ func mkdir(t *testing.T, cli *sftp.Client, name string) { } } +// copyFile copies a file from the local machine to the remote machine. func copyFile(t *testing.T, cli *sftp.Client, localSrc, remoteDest string) { t.Helper() @@ -344,6 +345,38 @@ func copyFile(t *testing.T, cli *sftp.Client, localSrc, remoteDest string) { } } +// copyFileFrom copies a file from the remote machine to the local machine +func copyFileFrom(t *testing.T, cli *sftp.Client, localDest, remoteSrc string) { + t.Helper() + + remoteFile, err := cli.Open(remoteSrc) + if err != nil { + t.Fatalf("can't open: %v", err) + } + defer remoteFile.Close() + + localFile, err := os.Create(localDest) + if err != nil { + t.Fatalf("can't open: %v", err) + } + defer localFile.Close() + + rfStat, err := remoteFile.Stat() + if err != nil { + t.Fatalf("can't stat: %v", err) + } + + n, err := io.Copy(localFile, remoteFile) + if err != nil { + t.Fatalf("copy failed: %v", err) + } + + if rfStat.Size() != n { + t.Fatalf("incorrect number of bytes copied: wanted: %d, got: %d", rfStat.Size(), n) + } + +} + const metaDataTemplate = `instance-id: {{.ID}} local-hostname: {{.Hostname}}` diff --git a/tstest/integration/vms/vms_test.go b/tstest/integration/vms/vms_test.go index 689cd5f8e..4ac7de732 100644 --- a/tstest/integration/vms/vms_test.go +++ b/tstest/integration/vms/vms_test.go @@ -11,9 +11,11 @@ "context" "flag" "fmt" + "io/ioutil" "net" "os" "os/exec" + "path" "path/filepath" "regexp" "strconv" @@ -487,6 +489,8 @@ func (h *Harness) testDistro(t *testing.T, d Distro, ipm ipMapping) { } }) + t.Run("taildrop", func(t *testing.T) { testTaildrop(t, h, cli) }) + t.Run("outgoing-udp-ipv4", func(t *testing.T) { cwd, err := os.Getwd() if err != nil { @@ -617,6 +621,66 @@ func (h *Harness) testDistro(t *testing.T, d Distro, ipm ipMapping) { }) } +func testTaildrop(t *testing.T, h *Harness, cli *ssh.Client) { + // local setup + src := t.TempDir() + dstDir := t.TempDir() + contents := []byte("Taildrop drop bop 💧 ??%@#@123˙©∆∆˚ 水平線") + + filename := "taildrop.txt" + filePath := path.Join(src, filename) + if err := ioutil.WriteFile(filePath, contents, 0666); err != nil { + t.Fatal(err) + } + + ipBytes, err := getSession(t, cli).Output("tailscale ip -4") + if err != nil { + t.Fatalf("can't run `tailscale ip -4`: %v", err) + } + target := string(bytes.TrimSpace(ipBytes)) + + // check that targets contains the IP we're sending to + output := h.Tailscale(t, "file", "cp", "-targets") + + if !bytes.Contains(output, []byte(target)) { + t.Errorf("Missing target from cp -targets, want: %s, in: %s", target, output) + } + + h.Tailscale(t, "file", "cp", filePath, target+":") + + out, err := getSession(t, cli).CombinedOutput( + fmt.Sprintf("tailscale file get -wait %s", dstDir), + ) + if err != nil { + t.Fatal(string(out), err) + } + + sftpDst, err := sftp.NewClient(cli) + if err != nil { + t.Fatalf("can't connect over sftp to copy file : %v", err) + } + defer sftpDst.Close() + copyFileFrom(t, sftpDst, path.Join(dstDir, filename), filename) + + files, err := ioutil.ReadDir(dstDir) + if err != nil { + t.Error(err) + } + if len(files) != 1 { + t.Fatalf("want 1 file, got %d", len(files)) + } + + gotFile := files[0] + got, err := ioutil.ReadFile(path.Join(dstDir, gotFile.Name())) + if err != nil { + t.Errorf("Failed to read from cp'd file: %v", err) + } + + if !bytes.Equal(got, contents) { + t.Errorf("mismatched taildrop contents, want %s, got %s", contents, got) + } +} + func runTestCommands(t *testing.T, timeout time.Duration, cli *ssh.Client, batch []expect.Batcher) { e, _, err := expect.SpawnSSH(cli, timeout, expect.Verbose(true),