2021-04-29 21:44:08 +00:00
|
|
|
// 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"
|
|
|
|
)
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
2021-04-29 22:58:29 +00:00
|
|
|
httpProxy := 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 httpProxy.Close()
|
|
|
|
|
2021-04-29 21:44:08 +00:00
|
|
|
socketPath := filepath.Join(td, "tailscale.sock")
|
|
|
|
dcmd := exec.Command(daemonExe,
|
|
|
|
"--tun=userspace-networking",
|
|
|
|
"--state="+filepath.Join(td, "tailscale.state"),
|
|
|
|
"--socket="+socketPath,
|
|
|
|
)
|
2021-04-29 22:58:29 +00:00
|
|
|
dcmd.Env = append(os.Environ(),
|
|
|
|
"TS_LOG_TARGET="+ts.URL,
|
|
|
|
"HTTP_PROXY="+httpProxy.URL,
|
|
|
|
"HTTPS_PROXY="+httpProxy.URL,
|
|
|
|
)
|
2021-04-29 21:44:08 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-04-29 22:58:29 +00:00
|
|
|
if os.Getenv("TS_RUN_TEST") == "failing_up" {
|
|
|
|
// Force a connection through the HTTP proxy to panic and fail.
|
|
|
|
exec.Command(cliExe, "--socket="+socketPath, "up").Run()
|
|
|
|
}
|
|
|
|
|
2021-04-29 21:44:08 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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("go", "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
|
|
|
|
}
|