summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--tstest/integration/integration_test.go93
-rw-r--r--tstest/integration/testcontrol/testcontrol.go117
2 files changed, 193 insertions, 17 deletions
diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go
index 922d6db3f..7a0d3bfcc 100644
--- a/tstest/integration/integration_test.go
+++ b/tstest/integration/integration_test.go
@@ -21,6 +21,7 @@ import (
"os/exec"
"path"
"path/filepath"
+ "regexp"
"runtime"
"strings"
"sync"
@@ -57,11 +58,7 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
-func TestIntegration(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("not tested/working on Windows yet")
- }
-
+func TestOneNodeUp_NoAuth(t *testing.T) {
bins := buildTestBinaries(t)
env := newTestEnv(t, bins)
@@ -127,6 +124,69 @@ func TestIntegration(t *testing.T) {
}
}
+func TestOneNodeUp_Auth(t *testing.T) {
+ bins := buildTestBinaries(t)
+
+ env := newTestEnv(t, bins)
+ defer env.Close()
+ env.Control.RequireAuth = true
+
+ n1 := newTestNode(t, env)
+
+ dcmd := n1.StartDaemon(t)
+ defer dcmd.Process.Kill()
+
+ n1.AwaitListening(t)
+
+ st := n1.MustStatus(t)
+ t.Logf("Status: %s", st.BackendState)
+
+ t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
+
+ cmd := n1.Tailscale("up", "--login-server="+env.ControlServer.URL)
+ cmd.Stdout = &authURLParserWriter{fn: func(urlStr string) error {
+ if env.Control.CompleteAuth(urlStr) {
+ t.Logf("completed auth path")
+ return nil
+ }
+ err := fmt.Errorf("Failed to complete auth path to %q", urlStr)
+ t.Log(err)
+ return err
+ }}
+ cmd.Stderr = cmd.Stdout
+ if err := cmd.Run(); err != nil {
+ t.Fatalf("up: %v", err)
+ }
+ var ip string
+ if err := tstest.WaitFor(20*time.Second, func() error {
+ out, err := n1.Tailscale("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", env.LogCatcher.numRequests())
+ if err := env.TrafficTrap.Err(); err != nil {
+ t.Errorf("traffic trap: %v", err)
+ t.Logf("logs: %s", env.LogCatcher.logsString())
+ }
+}
+
// testBinaries are the paths to a tailscaled and tailscale binary.
// These can be shared by multiple nodes.
type testBinaries struct {
@@ -168,6 +228,9 @@ type testEnv struct {
//
// Call Close to shut everything down.
func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
+ if runtime.GOOS == "windows" {
+ t.Skip("not tested/working on Windows yet")
+ }
derpMap, derpShutdown := runDERPAndStun(t, logger.Discard)
logc := new(logCatcher)
control := &testcontrol.Server{
@@ -184,6 +247,7 @@ func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
TrafficTrapServer: httptest.NewServer(trafficTrap),
derpShutdown: derpShutdown,
}
+ e.Control.BaseURL = e.ControlServer.URL
return e
}
@@ -454,3 +518,22 @@ func runDERPAndStun(t testing.TB, logf logger.Logf) (derpMap *tailcfg.DERPMap, c
return m, cleanup
}
+
+type authURLParserWriter struct {
+ buf bytes.Buffer
+ fn func(urlStr string) error
+}
+
+var authURLRx = regexp.MustCompile(`(https?://\S+/auth/\S+)`)
+
+func (w *authURLParserWriter) Write(p []byte) (n int, err error) {
+ n, err = w.buf.Write(p)
+ m := authURLRx.FindSubmatch(w.buf.Bytes())
+ if m != nil {
+ urlStr := string(m[1])
+ if err := w.fn(urlStr); err != nil {
+ return 0, err
+ }
+ }
+ return n, err
+}
diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go
index fe3a12911..9ad44a22c 100644
--- a/tstest/integration/testcontrol/testcontrol.go
+++ b/tstest/integration/testcontrol/testcontrol.go
@@ -17,6 +17,7 @@ import (
"log"
"math/rand"
"net/http"
+ "net/url"
"strings"
"sync"
"time"
@@ -34,19 +35,39 @@ import (
// Server is a control plane server. Its zero value is ready for use.
// Everything is stored in-memory in one tailnet.
type Server struct {
- Logf logger.Logf // nil means to use the log package
- DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
+ Logf logger.Logf // nil means to use the log package
+ DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
+ RequireAuth bool
+ BaseURL string // must be set to e.g. "http://127.0.0.1:1234" with no trailing URL
initMuxOnce sync.Once
mux *http.ServeMux
- mu sync.Mutex
- pubKey wgkey.Key
- privKey wgkey.Private
- nodes map[tailcfg.NodeKey]*tailcfg.Node
- users map[tailcfg.NodeKey]*tailcfg.User
- logins map[tailcfg.NodeKey]*tailcfg.Login
- updates map[tailcfg.NodeID]chan updateType
+ mu sync.Mutex
+ pubKey wgkey.Key
+ privKey wgkey.Private
+ nodes map[tailcfg.NodeKey]*tailcfg.Node
+ users map[tailcfg.NodeKey]*tailcfg.User
+ logins map[tailcfg.NodeKey]*tailcfg.Login
+ updates map[tailcfg.NodeID]chan updateType
+ authPath map[string]*AuthPath
+}
+
+type AuthPath struct {
+ closeOnce sync.Once
+ ch chan struct{}
+ success bool
+}
+
+func (ap *AuthPath) completeSuccessfully() {
+ ap.success = true
+ close(ap.ch)
+}
+
+// CompleteSuccessfully completes the login path successfully, as if
+// the user did the whole auth dance.
+func (ap *AuthPath) CompleteSuccessfully() {
+ ap.closeOnce.Do(ap.completeSuccessfully)
}
func (s *Server) logf(format string, a ...interface{}) {
@@ -178,6 +199,48 @@ func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login
return user, login
}
+// authPathDone returns a close-only struct that's closed when the
+// authPath ("/auth/XXXXXX") has authenticated.
+func (s *Server) authPathDone(authPath string) <-chan struct{} {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if a, ok := s.authPath[authPath]; ok {
+ return a.ch
+ }
+ return nil
+}
+
+func (s *Server) addAuthPath(authPath string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.authPath == nil {
+ s.authPath = map[string]*AuthPath{}
+ }
+ s.authPath[authPath] = &AuthPath{
+ ch: make(chan struct{}),
+ }
+}
+
+// CompleteAuth marks the provided path or URL (containing
+// "/auth/...") as successfully authenticated, unblocking any
+// requests blocked on that in serveRegister.
+func (s *Server) CompleteAuth(authPathOrURL string) bool {
+ i := strings.Index(authPathOrURL, "/auth/")
+ if i == -1 {
+ return false
+ }
+ authPath := authPathOrURL[i:]
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ ap, ok := s.authPath[authPath]
+ if !ok {
+ return false
+ }
+ ap.CompleteSuccessfully()
+ return true
+}
+
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
var req tailcfg.RegisterRequest
if err := s.decode(mkey, r.Body, &req); err != nil {
@@ -190,27 +253,57 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tail
panic("serveRegister: request has zero node key")
}
+ // If this is a followup request, wait until interactive followup URL visit complete.
+ if req.Followup != "" {
+ followupURL, err := url.Parse(req.Followup)
+ if err != nil {
+ panic(err)
+ }
+ doneCh := s.authPathDone(followupURL.Path)
+ select {
+ case <-r.Context().Done():
+ return
+ case <-doneCh:
+ }
+ // TODO(bradfitz): support a side test API to mark an
+ // auth as failued so we can send an error response in
+ // some follow-ups? For now all are successes.
+ }
+
user, login := s.getUser(req.NodeKey)
s.mu.Lock()
if s.nodes == nil {
s.nodes = map[tailcfg.NodeKey]*tailcfg.Node{}
}
+
+ machineAuthorized := true // TODO: add Server.RequireMachineAuth
+
s.nodes[req.NodeKey] = &tailcfg.Node{
ID: tailcfg.NodeID(user.ID),
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(user.ID))),
User: user.ID,
Machine: mkey,
Key: req.NodeKey,
- MachineAuthorized: true,
+ MachineAuthorized: machineAuthorized,
}
s.mu.Unlock()
+ authURL := ""
+ if s.RequireAuth {
+ machineAuthorized = false
+ randHex := make([]byte, 10)
+ crand.Read(randHex)
+ authPath := fmt.Sprintf("/auth/%x", randHex)
+ s.addAuthPath(authPath)
+ authURL = s.BaseURL + authPath
+ }
+
res, err := s.encode(mkey, false, tailcfg.RegisterResponse{
User: *user,
Login: *login,
NodeKeyExpired: false,
- MachineAuthorized: true,
- AuthURL: "", // all good; TODO(bradfitz): add ways to not start all good.
+ MachineAuthorized: machineAuthorized,
+ AuthURL: authURL,
})
if err != nil {
go panic(fmt.Sprintf("serveRegister: encode: %v", err))