tstest/integration: add testNode.AwaitListening, DERP+STUN, improve proxy trap

Updates #1840
pull/1842/head
Brad Fitzpatrick 4 years ago
parent 1f48d3556f
commit 3237e140c4

@ -26,11 +26,11 @@ type stunStats struct {
readIPv6 int readIPv6 int
} }
func Serve(t *testing.T) (addr *net.UDPAddr, cleanupFn func()) { func Serve(t testing.TB) (addr *net.UDPAddr, cleanupFn func()) {
return ServeWithPacketListener(t, nettype.Std{}) return ServeWithPacketListener(t, nettype.Std{})
} }
func ServeWithPacketListener(t *testing.T, ln nettype.PacketListener) (addr *net.UDPAddr, cleanupFn func()) { func ServeWithPacketListener(t testing.TB, ln nettype.PacketListener) (addr *net.UDPAddr, cleanupFn func()) {
t.Helper() t.Helper()
// TODO(crawshaw): use stats to test re-STUN logic // TODO(crawshaw): use stats to test re-STUN logic
@ -52,7 +52,7 @@ func ServeWithPacketListener(t *testing.T, ln nettype.PacketListener) (addr *net
} }
} }
func runSTUN(t *testing.T, pc net.PacketConn, stats *stunStats, done chan<- struct{}) { func runSTUN(t testing.TB, pc net.PacketConn, stats *stunStats, done chan<- struct{}) {
defer close(done) defer close(done)
var buf [64 << 10]byte var buf [64 << 10]byte

@ -7,11 +7,14 @@ package integration
import ( import (
"bytes" "bytes"
crand "crypto/rand"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -21,15 +24,38 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
"go4.org/mem" "go4.org/mem"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/net/stun/stuntest"
"tailscale.com/safesocket"
"tailscale.com/smallzstd" "tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/tstest/integration/testcontrol" "tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
) )
var mainError atomic.Value // of error
func TestMain(m *testing.M) {
v := m.Run()
if v != 0 {
os.Exit(v)
}
if err, ok := mainError.Load().(error); ok {
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func TestIntegration(t *testing.T) { func TestIntegration(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("not tested/working on Windows yet") t.Skip("not tested/working on Windows yet")
@ -37,7 +63,7 @@ func TestIntegration(t *testing.T) {
bins := buildTestBinaries(t) bins := buildTestBinaries(t)
env := newTestEnv(bins) env := newTestEnv(t, bins)
defer env.Close() defer env.Close()
n1 := newTestNode(t, env) n1 := newTestNode(t, env)
@ -45,16 +71,13 @@ func TestIntegration(t *testing.T) {
dcmd := n1.StartDaemon(t) dcmd := n1.StartDaemon(t)
defer dcmd.Process.Kill() defer dcmd.Process.Kill()
var json []byte n1.AwaitListening(t)
if err := tstest.WaitFor(20*time.Second, func() (err error) {
json, err = n1.Tailscale("status", "--json").CombinedOutput() json, err := n1.Tailscale("status", "--json").CombinedOutput()
if err != nil { if err != nil {
return fmt.Errorf("running tailscale status: %v, %s", err, json) t.Fatalf("running tailscale status: %v, %s", err, json)
}
return nil
}); err != nil {
t.Fatal(err)
} }
t.Logf("Status: %s", json)
if err := tstest.WaitFor(20*time.Second, func() error { if err := tstest.WaitFor(20*time.Second, func() error {
const sub = `Program starting: ` const sub = `Program starting: `
@ -66,10 +89,16 @@ func TestIntegration(t *testing.T) {
t.Error(err) t.Error(err)
} }
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
if err := n1.Tailscale("up", "--login-server="+env.ControlServer.URL).Run(); err != nil { if err := n1.Tailscale("up", "--login-server="+env.ControlServer.URL).Run(); err != nil {
t.Fatalf("up: %v", err) t.Fatalf("up: %v", err)
} }
if d, _ := time.ParseDuration(os.Getenv("TS_POST_UP_SLEEP")); d > 0 {
t.Logf("Sleeping for %v to give 'up' time to misbehave (https://github.com/tailscale/tailscale/issues/1840) ...", d)
time.Sleep(d)
}
var ip string var ip string
if err := tstest.WaitFor(20*time.Second, func() error { if err := tstest.WaitFor(20*time.Second, func() error {
out, err := n1.Tailscale("ip").Output() out, err := n1.Tailscale("ip").Output()
@ -94,6 +123,10 @@ func TestIntegration(t *testing.T) {
} }
t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests()) t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests())
if err, ok := mainError.Load().(error); ok {
t.Error(err)
t.Logf("logs: %s", env.LogCatcher.logsString())
}
} }
// testBinaries are the paths to a tailscaled and tailscale binary. // testBinaries are the paths to a tailscaled and tailscale binary.
@ -131,29 +164,40 @@ type testEnv struct {
// so any accidental traffic leaving tailscaled goes here and fails // so any accidental traffic leaving tailscaled goes here and fails
// the test. (localhost traffic bypasses HTTP_PROXY) // the test. (localhost traffic bypasses HTTP_PROXY)
CatchBadTrafficServer *httptest.Server CatchBadTrafficServer *httptest.Server
derpShutdown func()
} }
// newTestEnv starts a bunch of services and returns a new test // newTestEnv starts a bunch of services and returns a new test
// environment. // environment.
// //
// Call Close to shut everything down. // Call Close to shut everything down.
func newTestEnv(bins *testBinaries) *testEnv { func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
derpMap, derpShutdown := runDERPAndStun(t, t.Logf)
logc := new(logCatcher) logc := new(logCatcher)
control := new(testcontrol.Server) control := &testcontrol.Server{
return &testEnv{ DERPMap: derpMap,
Binaries: bins,
LogCatcher: logc,
LogCatcherServer: httptest.NewServer(logc),
CatchBadTrafficServer: httptest.NewServer(http.HandlerFunc(catchUnexpectedTraffic)),
Control: control,
ControlServer: httptest.NewServer(control),
} }
e := &testEnv{
Binaries: bins,
LogCatcher: logc,
LogCatcherServer: httptest.NewServer(logc),
Control: control,
ControlServer: httptest.NewServer(control),
derpShutdown: derpShutdown,
}
e.CatchBadTrafficServer = httptest.NewServer(http.HandlerFunc(e.catchUnexpectedTraffic))
return e
} }
func (e *testEnv) Close() error { func (e *testEnv) Close() error {
e.LogCatcherServer.Close() e.LogCatcherServer.Close()
e.CatchBadTrafficServer.Close() e.CatchBadTrafficServer.Close()
e.ControlServer.Close() e.ControlServer.Close()
e.derpShutdown()
return nil return nil
} }
@ -199,6 +243,21 @@ func (n *testNode) StartDaemon(t testing.TB) *exec.Cmd {
return cmd return cmd
} }
// AwaitListening waits for the tailscaled to be serving local clients
// over its localhost IPC mechanism. (Unix socket, etc)
func (n *testNode) AwaitListening(t testing.TB) {
if err := tstest.WaitFor(20*time.Second, func() (err error) {
c, err := safesocket.Connect(n.sockFile, 41112)
if err != nil {
return err
}
c.Close()
return nil
}); err != nil {
t.Fatal(err)
}
}
// Tailscale returns a command that runs the tailscale CLI with the provided arguments. // Tailscale returns a command that runs the tailscale CLI with the provided arguments.
// It does not start the process. // It does not start the process.
func (n *testNode) Tailscale(arg ...string) *exec.Cmd { func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
@ -317,12 +376,58 @@ func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) // must have no content, but not a 204 w.WriteHeader(200) // must have no content, but not a 204
} }
// catchUnexpectedTraffic is an HTTP proxy handler to blow up // catchUnexpectedTraffic is an HTTP proxy handler to note whether any
// if any HTTP traffic tries to leave localhost from // HTTP traffic tries to leave localhost from tailscaled. We don't
// tailscaled. // expect any, so any request triggers a failure.
func catchUnexpectedTraffic(w http.ResponseWriter, r *http.Request) { func (e *testEnv) catchUnexpectedTraffic(w http.ResponseWriter, r *http.Request) {
var got bytes.Buffer var got bytes.Buffer
r.Write(&got) r.Write(&got)
err := fmt.Errorf("unexpected HTTP proxy via proxy: %s", got.Bytes()) err := fmt.Errorf("unexpected HTTP proxy via proxy: %s", got.Bytes())
go panic(err) mainError.Store(err)
log.Printf("Error: %v", err)
}
func runDERPAndStun(t testing.TB, logf logger.Logf) (derpMap *tailcfg.DERPMap, cleanup func()) {
var serverPrivateKey key.Private
if _, err := crand.Read(serverPrivateKey[:]); err != nil {
t.Fatal(err)
}
d := derp.NewServer(serverPrivateKey, logf)
httpsrv := httptest.NewUnstartedServer(derphttp.Handler(d))
httpsrv.Config.ErrorLog = logger.StdLogger(logf)
httpsrv.Config.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
httpsrv.StartTLS()
stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
m := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
RegionID: 1,
RegionCode: "test",
Nodes: []*tailcfg.DERPNode{
{
Name: "t1",
RegionID: 1,
HostName: "127.0.0.1", // to bypass HTTP proxy
IPv4: "127.0.0.1",
IPv6: "none",
STUNPort: stunAddr.Port,
DERPTestPort: httpsrv.Listener.Addr().(*net.TCPAddr).Port,
STUNTestIP: stunAddr.IP.String(),
},
},
},
},
}
cleanup = func() {
httpsrv.CloseClientConnections()
httpsrv.Close()
d.Close()
stunCleanup()
}
return m, cleanup
} }

Loading…
Cancel
Save