// Copyright (c) 2020 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. // +build depends_on_currently_unreleased package controlclient import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/http/httptest" "net/url" "os" "reflect" "runtime/pprof" "strconv" "strings" "sync" "testing" "time" "github.com/klauspost/compress/zstd" "github.com/tailscale/wireguard-go/wgcfg" "tailscale.com/tailcfg" "tailscale.com/testy" "tailscale.io/control" // not yet released ) func TestTest(t *testing.T) { check := testy.NewResourceCheck() defer check.Assert(t) } func TestServerStartStop(t *testing.T) { s := newServer(t) defer s.close() } func TestControlBasics(t *testing.T) { s := newServer(t) defer s.close() c := s.newClient(t, "c") c.Login(nil, 0) status := c.waitStatus(t, stateURLVisitRequired) c.postAuthURL(t, "foo@tailscale.com", status.New) } func TestControl(t *testing.T) { log.SetFlags(log.Ltime | log.Lshortfile) s := newServer(t) defer s.close() c1 := s.newClient(t, "c1") t.Run("authorize first tailscale.com client", func(t *testing.T) { const loginName = "testuser1@tailscale.com" c1.checkNoStatus(t) c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) status := c1.waitStatus(t, stateSynchronized) if got, want := status.New.NetMap.MachineStatus, tailcfg.MachineUnauthorized; got != want { t.Errorf("MachineStatus=%v, want %v", got, want) } c1.checkNoStatus(t) affectedPeers, err := s.control.AuthorizeMachine(c1.mkey, c1.nkey) if err != nil { t.Fatal(err) } status = c1.status(t) if got := status.New.Persist.LoginName; got != loginName { t.Errorf("LoginName=%q, want %q", got, loginName) } if got := status.New.Persist.Provider; got != "google" { t.Errorf("Provider=%q, want google", got) } if len(affectedPeers) != 1 || affectedPeers[0] != c1.id { t.Errorf("authorization should notify the node being authorized (%v), got: %v", c1.id, affectedPeers) } if peers := status.New.NetMap.Peers; len(peers) != 0 { t.Errorf("peers=%v, want none", peers) } if userID := status.New.NetMap.User; userID == 0 { t.Errorf("NetMap.User is missing") } else { profile := status.New.NetMap.UserProfiles[userID] if profile.LoginName != loginName { t.Errorf("NetMap user LoginName=%q, want %q", profile.LoginName, loginName) } } c1.checkNoStatus(t) }) c2 := s.newClient(t, "c2") t.Run("authorize second tailscale.io client", func(t *testing.T) { c2.loginAs(t, "testuser2@tailscale.com") c2.waitStatus(t, stateAuthenticated) c2.waitStatus(t, stateSynchronized) c2.checkNoStatus(t) // Make sure not to call operations like this on a client in a // test until the initial map read is done. Otherwise the // initial map read will trigger a map update to peers, and // there will sometimes be a spurious map update. affectedPeers, err := s.control.AuthorizeMachine(c2.mkey, c2.nkey) if err != nil { t.Fatal(err) } status := c2.waitStatus(t, stateSynchronized) c1Status := c1.waitStatus(t, stateSynchronized) if len(affectedPeers) != 2 { t.Errorf("affectedPeers=%v, want two entries", affectedPeers) } if want := []tailcfg.NodeID{c1.id, c2.id}; !nodeIDsEqual(affectedPeers, want) { t.Errorf("affectedPeers=%v, want %v", affectedPeers, want) } c1NetMap := c1Status.New.NetMap c2NetMap := status.New.NetMap if len(c1NetMap.Peers) != 1 || len(c2NetMap.Peers) != 1 { t.Error("wrong number of peers") } else { if c2NetMap.Peers[0].Key != c1.nkey { t.Errorf("c2 has wrong peer key %v, want %v", c2NetMap.Peers[0].Key, c1.nkey) } if c1NetMap.Peers[0].Key != c2.nkey { t.Errorf("c1 has wrong peer key %v, want %v", c1NetMap.Peers[0].Key, c2.nkey) } } if t.Failed() { t.Errorf("client1 network map:\n%s", c1Status.New.NetMap) t.Errorf("client2 network map:\n%s", status.New.NetMap) } c1.checkNoStatus(t) c2.checkNoStatus(t) }) // c3/c4 are on a different domain to c1/c2. // The two domains should never affect one another. c3 := s.newClient(t, "c3") t.Run("authorize first onmicrosoft client", func(t *testing.T) { c3.loginAs(t, "testuser1@tailscale.onmicrosoft.com") c3.waitStatus(t, stateAuthenticated) c3Status := c3.waitStatus(t, stateSynchronized) // no machine authorization for tailscale.onmicrosoft.com c1.checkNoStatus(t) c2.checkNoStatus(t) netMap := c3Status.New.NetMap if netMap.NodeKey != c3.nkey { t.Errorf("netMap.NodeKey=%v, want %v", netMap.NodeKey, c3.nkey) } if len(netMap.Peers) != 0 { t.Errorf("netMap.Peers=%v, want none", netMap.Peers) } c1.checkNoStatus(t) c2.checkNoStatus(t) c3.checkNoStatus(t) }) c4 := s.newClient(t, "c4") t.Run("authorize second onmicrosoft client", func(t *testing.T) { c4.loginAs(t, "testuser2@tailscale.onmicrosoft.com") c4.waitStatus(t, stateAuthenticated) c3Status := c3.waitStatus(t, stateSynchronized) c4Status := c4.waitStatus(t, stateSynchronized) c3NetMap := c3Status.New.NetMap c4NetMap := c4Status.New.NetMap c1.checkNoStatus(t) c2.checkNoStatus(t) if len(c3NetMap.Peers) != 1 { t.Errorf("wrong number of c3 peers: %d", len(c3NetMap.Peers)) } else if len(c4NetMap.Peers) != 1 { t.Errorf("wrong number of c4 peers: %d", len(c4NetMap.Peers)) } else { if c3NetMap.Peers[0].Key != c4.nkey || c4NetMap.Peers[0].Key != c3.nkey { t.Error("wrong peer key") } } if t.Failed() { t.Errorf("client3 network map:\n%s", c3NetMap) t.Errorf("client4 network map:\n%s", c4NetMap) } }) var c1NetMap *NetworkMap t.Run("update c1 and c2 endpoints", func(t *testing.T) { c1Endpoints := []string{"172.16.1.5:12345", "4.4.4.4:4444"} c1.checkNoStatus(t) c1.UpdateEndpoints(1234, c1Endpoints) c1NetMap = c1.status(t).New.NetMap c2NetMap := c2.status(t).New.NetMap c1.checkNoStatus(t) c2.checkNoStatus(t) if c1NetMap.LocalPort != 1234 { t.Errorf("c1 netmap localport=%d, want 1234", c1NetMap.LocalPort) } if len(c2NetMap.Peers) != 1 { t.Fatalf("wrong peer count: %d", len(c2NetMap.Peers)) } if got := c2NetMap.Peers[0].Endpoints; !reflect.DeepEqual(c1Endpoints, got) { t.Errorf("c2 peer endpoints=%v, want %v", got, c1Endpoints) } c3.checkNoStatus(t) c4.checkNoStatus(t) c2Endpoints := []string{"172.16.1.7:6543", "5.5.5.5.3333"} c2.UpdateEndpoints(9876, c2Endpoints) c1NetMap = c1.status(t).New.NetMap c2NetMap = c2.status(t).New.NetMap if c1NetMap.LocalPort != 1234 { t.Errorf("c1 netmap localport=%d, want 1234", c1NetMap.LocalPort) } if c2NetMap.LocalPort != 9876 { t.Errorf("c2 netmap localport=%d, want 9876", c2NetMap.LocalPort) } if got := c2NetMap.Peers[0].Endpoints; !reflect.DeepEqual(c1Endpoints, got) { t.Errorf("c2 peer endpoints=%v, want %v", got, c1Endpoints) } if got := c1NetMap.Peers[0].Endpoints; !reflect.DeepEqual(c2Endpoints, got) { t.Errorf("c1 peer endpoints=%v, want %v", got, c2Endpoints) } c1.checkNoStatus(t) c2.checkNoStatus(t) c3.checkNoStatus(t) c4.checkNoStatus(t) }) allZeros, err := wgcfg.ParseCIDR("0.0.0.0/0") if err != nil { t.Fatal(err) } t.Run("route all traffic via client 1", func(t *testing.T) { aips := []wgcfg.CIDR{} aips = append(aips, c1NetMap.Addresses...) aips = append(aips, *allZeros) affectedPeers, err := s.control.SetAllowedIPs(c1.nkey, aips) if err != nil { t.Fatal(err) } c2Status := c2.status(t) c2NetMap := c2Status.New.NetMap if want := []tailcfg.NodeID{c2.id}; !nodeIDsEqual(affectedPeers, want) { t.Errorf("affectedPeers=%v, want %v", affectedPeers, want) } _ = c2NetMap foundAllZeros := false for _, cidr := range c2NetMap.Peers[0].AllowedIPs { if cidr == *allZeros { foundAllZeros = true } } if !foundAllZeros { t.Errorf("client2 peer does not contain %s: %v", allZeros, c2NetMap.Peers[0].AllowedIPs) } c1.checkNoStatus(t) c3.checkNoStatus(t) c4.checkNoStatus(t) }) t.Run("remove route all traffic", func(t *testing.T) { affectedPeers, err := s.control.SetAllowedIPs(c1.nkey, c1NetMap.Addresses) if err != nil { t.Fatal(err) } c2NetMap := c2.status(t).New.NetMap if want := []tailcfg.NodeID{c2.id}; !nodeIDsEqual(affectedPeers, want) { t.Errorf("affectedPeers=%v, want %v", affectedPeers, want) } foundAllZeros := false for _, cidr := range c2NetMap.Peers[0].AllowedIPs { if cidr == *allZeros { foundAllZeros = true } } if foundAllZeros { t.Errorf("client2 peer still contains %s: %v", allZeros, c2NetMap.Peers[0].AllowedIPs) } c1.checkNoStatus(t) c3.checkNoStatus(t) c4.checkNoStatus(t) }) t.Run("refresh client key", func(t *testing.T) { oldKey := c1.nkey c1.Login(nil, LoginInteractive) status := c1.waitStatus(t, stateURLVisitRequired) authURL := status.New.URL resp, err := c1.httpc.Get(authURL) if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Errorf("GET %s failed: %q", authURL, resp.Status) } body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { t.Fatal(err) } cookies := resp.Cookies() if len(cookies) == 0 || cookies[0].Name != "tailcontrol" { t.Logf("GET %s: %s", authURL, string(body)) t.Fatalf("GET %s: bad cookie: %v", authURL, cookies) } c1.waitStatus(t, stateAuthenticated) status = c1.waitStatus(t, stateSynchronized) if status.New.Err != "" { t.Fatal(status.New.Err) } c1NetMap := status.New.NetMap c1.nkey = c1NetMap.NodeKey if c1.nkey == oldKey { t.Errorf("new key is the same as the old key: %s", oldKey) } c2NetMap := c2.status(t).New.NetMap if len(c2NetMap.Peers) != 1 || c2NetMap.Peers[0].Key != c1.nkey { t.Errorf("c2 peer: %v, want new node key %v", c1.nkey, c2NetMap.Peers[0].Key) } c3.checkNoStatus(t) c4.checkNoStatus(t) }) } func TestLoginInterrupt(t *testing.T) { s := newServer(t) defer s.close() c := s.newClient(t, "c") const loginName = "testuser1@tailscale.com" c.checkNoStatus(t) c.loginAs(t, loginName) c.waitStatus(t, stateAuthenticated) c.waitStatus(t, stateSynchronized) t.Logf("authorizing: %v %v %v %v\n", s, s.control, c.mkey, c.nkey) if _, err := s.control.AuthorizeMachine(c.mkey, c.nkey); err != nil { t.Fatal(err) } status := c.waitStatus(t, stateSynchronized) if got, want := status.New.NetMap.MachineStatus, tailcfg.MachineAuthorized; got != want { t.Errorf("MachineStatus=%v, want %v", got, want) } origAddrs := status.New.NetMap.Addresses if len(origAddrs) == 0 { t.Errorf("Addresses empty, want something") } c.Logout() c.waitStatus(t, stateNotAuthenticated) c.Login(nil, 0) status = c.waitStatus(t, stateURLVisitRequired) authURL := status.New.URL // Interrupt, and do login again. c.Login(nil, 0) status = c.waitStatus(t, stateURLVisitRequired) authURL2 := status.New.URL if authURL == authURL2 { t.Errorf("auth URLs match for subsequent logins: %s", authURL) } form := url.Values{"user": []string{loginName}} req, err := http.NewRequest("POST", authURL2, strings.NewReader(form.Encode())) if err != nil { t.Fatal(err) } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := c.httpc.Do(req.WithContext(c.ctx)) if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Fatalf("POST %s failed: %q", authURL2, resp.Status) } cookies := resp.Cookies() if len(cookies) == 0 || cookies[0].Name != "tailcontrol" { t.Fatalf("POST %s: bad cookie: %v", authURL2, cookies) } c.waitStatus(t, stateAuthenticated) status = c.status(t) if got := status.New.NetMap.NodeKey; got != c.nkey { t.Errorf("netmap has wrong node key: %v, want %v", got, c.nkey) } if got := status.New.NetMap.Addresses; len(got) == 0 { t.Errorf("Addresses empty after re-login, want something") } else if len(origAddrs) > 0 && origAddrs[0] != got[0] { t.Errorf("Addresses=%v after re-login, originally was %v, want IP to be unchanged", got, origAddrs) } } func TestSpinUpdateEndpoints(t *testing.T) { s := newServer(t) defer s.close() c1 := s.newClient(t, "c1") c2 := s.newClient(t, "c2") const loginName = "testuser1@tailscale.com" c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) c1.waitStatus(t, stateSynchronized) if _, err := s.control.AuthorizeMachine(c1.mkey, c1.nkey); err != nil { t.Fatal(err) } c1.waitStatus(t, stateSynchronized) c2.loginAs(t, loginName) c2.waitStatus(t, stateAuthenticated) c2.waitStatus(t, stateSynchronized) if _, err := s.control.AuthorizeMachine(c2.mkey, c2.nkey); err != nil { t.Fatal(err) } c2.waitStatus(t, stateSynchronized) c1.waitStatus(t, stateSynchronized) const portBase = 1200 const portCount = 50 const portLast = portBase + portCount - 1 errCh := make(chan error, 1) collectPorts := func() error { t := time.After(10 * time.Second) var port int for i := 0; i < portCount; i++ { var status statusChange select { case status = <-c2.statusCh: case <-t: return fmt.Errorf("c2 status timeout (i=%d)", i) } peers := status.New.NetMap.Peers if len(peers) != 1 { return fmt.Errorf("c2 len(peers)=%d, want 1", len(peers)) } eps := peers[0].Endpoints if len(eps) != 2 { return fmt.Errorf("c2 peer len(eps)=%d, want 2", len(eps)) } ep := eps[1] const prefix = "192.168.1.45:" if !strings.HasPrefix(ep, prefix) { return fmt.Errorf("c2 peer endpoint=%s, want prefix %s", ep, prefix) } var err error port, err = strconv.Atoi(strings.TrimPrefix(ep, prefix)) if err != nil { return fmt.Errorf("c2 peer endpoint port: %v", err) } if port == portLast { return nil // got it } } return fmt.Errorf("c2 peer endpoint did not see portLast (saw %d)", port) } go func() { errCh <- collectPorts() }() // Very quickly call UpdateEndpoints several times. // Some (most) of these calls will never make it to the server, they // will be canceled by subsequent calls. // The last call goes through, so we can see portLast. eps := []string{"127.0.0.1:1234", ""} for i := 0; i < portCount; i++ { eps[1] = fmt.Sprintf("192.168.1.45:%d", portBase+i) c1.UpdateEndpoints(1234, eps) } if err := <-errCh; err != nil { t.Fatalf("collect ports: %v", err) } } func TestLogout(t *testing.T) { s := newServer(t) defer s.close() c1 := s.newClient(t, "c1") const loginName = "testuser1@tailscale.com" c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) c1.waitStatus(t, stateSynchronized) if _, err := s.control.AuthorizeMachine(c1.mkey, c1.nkey); err != nil { t.Fatal(err) } nkey1 := c1.status(t).New.NetMap.NodeKey c1.Logout() c1.waitStatus(t, stateNotAuthenticated) c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) status := c1.waitStatus(t, stateSynchronized) if got, want := status.New.NetMap.MachineStatus, tailcfg.MachineAuthorized; got != want { t.Errorf("re-login MachineStatus=%v, want %v", got, want) } nkey2 := status.New.NetMap.NodeKey if nkey1 == nkey2 { t.Errorf("key not changed after re-login: %v", nkey1) } c1.checkNoStatus(t) } func TestExpiry(t *testing.T) { var nowMu sync.Mutex now := time.Now() // Server and Client use this variable as the current time timeNow := func() time.Time { nowMu.Lock() defer nowMu.Unlock() return now } timeInc := func(d time.Duration) { nowMu.Lock() defer nowMu.Unlock() now = now.Add(d) } s := newServer(t) s.control.TimeNow = timeNow defer s.close() c1 := s.newClient(t, "c1") const loginName = "testuser1@tailscale.com" c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) c1.waitStatus(t, stateSynchronized) if _, err := s.control.AuthorizeMachine(c1.mkey, c1.nkey); err != nil { t.Fatal(err) } status := c1.waitStatus(t, stateSynchronized).New nkey1 := c1.direct.persist.PrivateNodeKey nkey1Expiry := status.NetMap.Expiry if wantExpiry := timeNow().Add(180 * 24 * time.Hour); !nkey1Expiry.Equal(wantExpiry) { t.Errorf("node key expiry = %v, want %v", nkey1Expiry, wantExpiry) } timeInc(1 * time.Hour) // move the clock forward c1.Login(nil, LoginInteractive) // refresh the key status = c1.waitStatus(t, stateURLVisitRequired).New c1.postAuthURL(t, loginName, status) c1.waitStatus(t, stateAuthenticated) status = c1.waitStatus(t, stateSynchronized).New if newKey := c1.direct.persist.PrivateNodeKey; newKey == nkey1 { t.Errorf("node key unchanged after LoginInteractive: %v", nkey1) } if want, got := timeNow().Add(180*24*time.Hour), status.NetMap.Expiry; !got.Equal(want) { t.Errorf("node key expiry = %v, want %v", got, want) } timeInc(2 * time.Hour) // move the clock forward c1.Login(nil, 0) c1.waitStatus(t, stateAuthenticated) c1.waitStatus(t, stateSynchronized) c1.checkNoStatus(t) // nothing happens, network map stays the same timeInc(180 * 24 * time.Hour) // move the clock past expiry c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) status = c1.waitStatus(t, stateSynchronized).New if got, want := c1.expiry, timeNow(); got.Equal(want) { t.Errorf("node key expiry = %v, want %v", got, want) } if c1.direct.persist.PrivateNodeKey == nkey1 { t.Errorf("node key after 37 hours is still %v", status.NetMap.NodeKey) } } func TestRefresh(t *testing.T) { var nowMu sync.Mutex now := time.Now() // Server and Client use this variable as the current time timeNow := func() time.Time { nowMu.Lock() defer nowMu.Unlock() return now } s := newServer(t) s.control.TimeNow = timeNow defer s.close() c1 := s.newClient(t, "c1") const loginName = "testuser1@versabank.com" // versabank cfgdb has 72 hour key expiry configured c1.loginAs(t, loginName) c1.waitStatus(t, stateAuthenticated) c1.waitStatus(t, stateSynchronized) if _, err := s.control.AuthorizeMachine(c1.mkey, c1.nkey); err != nil { t.Fatal(err) } status := c1.status(t).New nkey1 := status.NetMap.NodeKey nkey1Expiry := status.NetMap.Expiry if wantExpiry := timeNow().Add(72 * time.Hour); !nkey1Expiry.Equal(wantExpiry) { t.Errorf("node key expiry = %v, want %v", nkey1Expiry, wantExpiry) } c1.Login(nil, LoginInteractive) c1.waitStatus(t, stateURLVisitRequired) // Until authorization happens, old netmap is still valid. exp := c1.expiry if exp == nil { t.Errorf("expiry==nil during refresh\n") } if got := *exp; !nkey1Expiry.Equal(got) { t.Errorf("node key expiry = %v, want %v", got, nkey1Expiry) } k := tailcfg.NodeKey(c1.direct.persist.PrivateNodeKey.Public()) if k != nkey1 { t.Errorf("node key after 2 hours is %v, want %v", k, nkey1) } c1.Shutdown() } func TestExpectedProvider(t *testing.T) { s := newServer(t) defer s.close() c := s.newClient(t, "c1") c.direct.persist.LoginName = "testuser1@tailscale.com" c.direct.persist.Provider = "microsoft" c.Login(nil, 0) status := c.readStatus(t) if e, substr := status.New.Err, `provider "microsoft" is not supported`; !strings.Contains(e, substr) { t.Errorf("Err=%q, expect substring %q", e, substr) } } func TestNewUserWebFlow(t *testing.T) { s := newServer(t) defer s.close() s.control.DB().SetSegmentAPIKey(segmentKey) c := s.newClient(t, "c1") c.Login(nil, 0) status := c.waitStatus(t, stateURLVisitRequired) authURL := status.New.URL resp, err := c.httpc.Get(authURL) if err != nil { t.Fatal(err) } if resp.StatusCode != 200 { t.Errorf("statuscode=%d, want 200", resp.StatusCode) } b, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } got := string(b) if !strings.Contains(got, ` stateAuthenticated // TODO: test os/hostname gets sent to server // TODO: test vpn IP not assigned until machine is authorized // TODO: test overlapping calls to RefreshLogin // TODO: test registering a new node for a user+machine key replaces the old // node even if the OldNodeKey is not specified by the client. // TODO: test "does not expire" on server extends expiry in sent network map