// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package integration contains Tailscale integration tests. package integration import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "log" "net/http" "net/http/httptest" "os" "os/exec" "path" "path/filepath" "runtime" "strings" "sync" "testing" "time" "go4.org/mem" "tailscale.com/smallzstd" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" ) func TestIntegration(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("not tested/working on Windows yet") } td := t.TempDir() daemonExe := build(t, td, "tailscale.com/cmd/tailscaled") cliExe := build(t, td, "tailscale.com/cmd/tailscale") logc := new(logCatcher) ts := httptest.NewServer(logc) defer ts.Close() // catchBadTrafficProxy explodes if it gets any traffic. // It's here to catch anything that would otherwise try to leave localhost. catchBadTrafficProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var got bytes.Buffer r.Write(&got) err := fmt.Errorf("unexpected HTTP proxy via proxy: %s", got.Bytes()) t.Error(err) go panic(err) })) defer catchBadTrafficProxy.Close() controlServer := new(testcontrol.Server) controlHTTPServer := httptest.NewServer(controlServer) defer controlHTTPServer.Close() socketPath := filepath.Join(td, "tailscale.sock") dcmd := exec.Command(daemonExe, "--tun=userspace-networking", "--state="+filepath.Join(td, "tailscale.state"), "--socket="+socketPath, ) dcmd.Env = append(os.Environ(), "TS_LOG_TARGET="+ts.URL, "HTTP_PROXY="+catchBadTrafficProxy.URL, "HTTPS_PROXY="+catchBadTrafficProxy.URL, ) if err := dcmd.Start(); err != nil { t.Fatalf("starting tailscaled: %v", err) } defer dcmd.Process.Kill() var json []byte if err := tstest.WaitFor(20*time.Second, func() (err error) { json, err = exec.Command(cliExe, "--socket="+socketPath, "status", "--json").CombinedOutput() if err != nil { return fmt.Errorf("running tailscale status: %v, %s", err, json) } return nil }); err != nil { t.Fatal(err) } if err := tstest.WaitFor(20*time.Second, func() error { const sub = `Program starting: ` if !logc.logsContains(mem.S(sub)) { return fmt.Errorf("log catcher didn't see %#q; got %s", sub, logc.logsString()) } return nil }); err != nil { t.Error(err) } if err := exec.Command(cliExe, "--socket="+socketPath, "up", "--login-server="+controlHTTPServer.URL).Run(); err != nil { t.Fatalf("up: %v", err) } var ip string if err := tstest.WaitFor(20*time.Second, func() error { out, err := exec.Command(cliExe, "--socket="+socketPath, "ip").Output() if err != nil { return err } ip = string(out) return nil }); err != nil { t.Error(err) } t.Logf("Got IP: %v", ip) dcmd.Process.Signal(os.Interrupt) ps, err := dcmd.Process.Wait() if err != nil { t.Fatalf("tailscaled Wait: %v", err) } if ps.ExitCode() != 0 { t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode()) } t.Logf("number of HTTP logcatcher requests: %v", logc.numRequests()) } func exe() string { if runtime.GOOS == "windows" { return ".exe" } return "" } func findGo(t *testing.T) string { goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe()) if fi, err := os.Stat(goBin); err != nil { if os.IsNotExist(err) { t.Fatalf("failed to find go at %v", goBin) } t.Fatalf("looking for go binary: %v", err) } else if !fi.Mode().IsRegular() { t.Fatalf("%v is unexpected %v", goBin, fi.Mode()) } t.Logf("using go binary %v", goBin) return goBin } func build(t *testing.T, outDir, target string) string { exe := "" if runtime.GOOS == "windows" { exe = ".exe" } bin := filepath.Join(outDir, path.Base(target)) + exe errOut, err := exec.Command(findGo(t), "build", "-o", bin, target).CombinedOutput() if err != nil { t.Fatalf("failed to build %v: %v, %s", target, err, errOut) } return bin } type logCatcher struct { mu sync.Mutex buf bytes.Buffer gotErr error reqs int } func (lc *logCatcher) logsContains(sub mem.RO) bool { lc.mu.Lock() defer lc.mu.Unlock() return mem.Contains(mem.B(lc.buf.Bytes()), sub) } func (lc *logCatcher) numRequests() int { lc.mu.Lock() defer lc.mu.Unlock() return lc.reqs } func (lc *logCatcher) logsString() string { lc.mu.Lock() defer lc.mu.Unlock() return lc.buf.String() } func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) { var body io.Reader = r.Body if r.Header.Get("Content-Encoding") == "zstd" { var err error body, err = smallzstd.NewDecoder(body) if err != nil { log.Printf("bad caught zstd: %v", err) http.Error(w, err.Error(), 400) return } } bodyBytes, _ := ioutil.ReadAll(body) type Entry struct { Logtail struct { ClientTime time.Time `json:"client_time"` ServerTime time.Time `json:"server_time"` Error struct { BadData string `json:"bad_data"` } `json:"error"` } `json:"logtail"` Text string `json:"text"` } var jreq []Entry var err error if len(bodyBytes) > 0 && bodyBytes[0] == '[' { err = json.Unmarshal(bodyBytes, &jreq) } else { var ent Entry err = json.Unmarshal(bodyBytes, &ent) jreq = append(jreq, ent) } lc.mu.Lock() defer lc.mu.Unlock() lc.reqs++ if lc.gotErr == nil && err != nil { lc.gotErr = err } if err != nil { fmt.Fprintf(&lc.buf, "error from %s of %#q: %v\n", r.Method, bodyBytes, err) } else { for _, ent := range jreq { fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text)) } } w.WriteHeader(200) // must have no content, but not a 204 }