Compare commits

...

1 Commits

Author SHA1 Message Date
julianknodt
b37da03e88 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 <julianknodt@gmail.com>
2021-07-23 15:31:42 -07:00
5 changed files with 222 additions and 20 deletions

View File

@@ -20,6 +20,7 @@ import (
"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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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}}`

View File

@@ -11,9 +11,11 @@ import (
"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),