From 3237e140c417c2f0bb7530bd4459511455507cba Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 3 May 2021 10:49:45 -0700 Subject: [PATCH] tstest/integration: add testNode.AwaitListening, DERP+STUN, improve proxy trap Updates #1840 --- net/stun/stuntest/stuntest.go | 6 +- tstest/integration/integration_test.go | 153 +++++++++++++++++++++---- 2 files changed, 132 insertions(+), 27 deletions(-) diff --git a/net/stun/stuntest/stuntest.go b/net/stun/stuntest/stuntest.go index 6015b9066..6057a43a9 100644 --- a/net/stun/stuntest/stuntest.go +++ b/net/stun/stuntest/stuntest.go @@ -26,11 +26,11 @@ type stunStats struct { 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{}) } -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() // 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) var buf [64 << 10]byte diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index c988df419..e78182f52 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -7,11 +7,14 @@ package integration import ( "bytes" + crand "crypto/rand" + "crypto/tls" "encoding/json" "fmt" "io" "io/ioutil" "log" + "net" "net/http" "net/http/httptest" "os" @@ -21,15 +24,38 @@ import ( "runtime" "strings" "sync" + "sync/atomic" "testing" "time" "go4.org/mem" + "tailscale.com/derp" + "tailscale.com/derp/derphttp" + "tailscale.com/net/stun/stuntest" + "tailscale.com/safesocket" "tailscale.com/smallzstd" + "tailscale.com/tailcfg" "tailscale.com/tstest" "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) { if runtime.GOOS == "windows" { t.Skip("not tested/working on Windows yet") @@ -37,7 +63,7 @@ func TestIntegration(t *testing.T) { bins := buildTestBinaries(t) - env := newTestEnv(bins) + env := newTestEnv(t, bins) defer env.Close() n1 := newTestNode(t, env) @@ -45,16 +71,13 @@ func TestIntegration(t *testing.T) { dcmd := n1.StartDaemon(t) defer dcmd.Process.Kill() - var json []byte - if err := tstest.WaitFor(20*time.Second, func() (err error) { - json, err = n1.Tailscale("status", "--json").CombinedOutput() - if err != nil { - return fmt.Errorf("running tailscale status: %v, %s", err, json) - } - return nil - }); err != nil { - t.Fatal(err) + n1.AwaitListening(t) + + json, err := n1.Tailscale("status", "--json").CombinedOutput() + if err != nil { + t.Fatalf("running tailscale status: %v, %s", err, json) } + t.Logf("Status: %s", json) if err := tstest.WaitFor(20*time.Second, func() error { const sub = `Program starting: ` @@ -66,10 +89,16 @@ func TestIntegration(t *testing.T) { 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 { 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 if err := tstest.WaitFor(20*time.Second, func() error { 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()) + 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. @@ -131,29 +164,40 @@ type testEnv struct { // so any accidental traffic leaving tailscaled goes here and fails // the test. (localhost traffic bypasses HTTP_PROXY) CatchBadTrafficServer *httptest.Server + + derpShutdown func() } // newTestEnv starts a bunch of services and returns a new test // environment. // // 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) - control := new(testcontrol.Server) - return &testEnv{ - Binaries: bins, - LogCatcher: logc, - LogCatcherServer: httptest.NewServer(logc), - CatchBadTrafficServer: httptest.NewServer(http.HandlerFunc(catchUnexpectedTraffic)), - Control: control, - ControlServer: httptest.NewServer(control), + control := &testcontrol.Server{ + DERPMap: derpMap, } + 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 { e.LogCatcherServer.Close() e.CatchBadTrafficServer.Close() e.ControlServer.Close() + e.derpShutdown() return nil } @@ -199,6 +243,21 @@ func (n *testNode) StartDaemon(t testing.TB) *exec.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. // It does not start the process. 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 } -// catchUnexpectedTraffic is an HTTP proxy handler to blow up -// if any HTTP traffic tries to leave localhost from -// tailscaled. -func catchUnexpectedTraffic(w http.ResponseWriter, r *http.Request) { +// catchUnexpectedTraffic is an HTTP proxy handler to note whether any +// HTTP traffic tries to leave localhost from tailscaled. We don't +// expect any, so any request triggers a failure. +func (e *testEnv) catchUnexpectedTraffic(w http.ResponseWriter, r *http.Request) { var got bytes.Buffer r.Write(&got) 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 }