| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipnserver_test
- import (
- "context"
- "errors"
- "runtime"
- "strconv"
- "sync"
- "testing"
- "tailscale.com/client/local"
- "tailscale.com/envknob"
- "tailscale.com/ipn"
- "tailscale.com/ipn/lapitest"
- "tailscale.com/tsd"
- "tailscale.com/types/ptr"
- "tailscale.com/util/syspolicy/pkey"
- "tailscale.com/util/syspolicy/policytest"
- )
- func TestUserConnectDisconnectNonWindows(t *testing.T) {
- enableLogging := false
- if runtime.GOOS == "windows" {
- setGOOSForTest(t, "linux")
- }
- ctx := context.Background()
- server := lapitest.NewServer(t, lapitest.WithLogging(enableLogging))
- // UserA connects and starts watching the IPN bus.
- clientA := server.ClientWithName("UserA")
- watcherA, _ := clientA.WatchIPNBus(ctx, 0)
- // The concept of "current user" is only relevant on Windows
- // and it should not be set on non-Windows platforms.
- server.CheckCurrentUser(nil)
- // Additionally, a different user should be able to connect and use the LocalAPI.
- clientB := server.ClientWithName("UserB")
- if _, gotErr := clientB.Status(ctx); gotErr != nil {
- t.Fatalf("Status(%q): want nil; got %v", clientB.Username(), gotErr)
- }
- // Watching the IPN bus should also work for UserB.
- watcherB, _ := clientB.WatchIPNBus(ctx, 0)
- // And if we send a notification, both users should receive it.
- wantErrMessage := "test error"
- testNotify := ipn.Notify{ErrMessage: ptr.To(wantErrMessage)}
- server.Backend().DebugNotify(testNotify)
- if n, err := watcherA.Next(); err != nil {
- t.Fatalf("IPNBusWatcher.Next(%q): %v", clientA.Username(), err)
- } else if gotErrMessage := n.ErrMessage; gotErrMessage == nil || *gotErrMessage != wantErrMessage {
- t.Fatalf("IPNBusWatcher.Next(%q): want %v; got %v", clientA.Username(), wantErrMessage, gotErrMessage)
- }
- if n, err := watcherB.Next(); err != nil {
- t.Fatalf("IPNBusWatcher.Next(%q): %v", clientB.Username(), err)
- } else if gotErrMessage := n.ErrMessage; gotErrMessage == nil || *gotErrMessage != wantErrMessage {
- t.Fatalf("IPNBusWatcher.Next(%q): want %v; got %v", clientB.Username(), wantErrMessage, gotErrMessage)
- }
- }
- func TestUserConnectDisconnectOnWindows(t *testing.T) {
- enableLogging := false
- setGOOSForTest(t, "windows")
- ctx := context.Background()
- server := lapitest.NewServer(t, lapitest.WithLogging(enableLogging))
- client := server.ClientWithName("User")
- _, cancelWatcher := client.WatchIPNBus(ctx, 0)
- // On Windows, however, the current user should be set to the user that connected.
- server.CheckCurrentUser(client.Actor)
- // Cancel the IPN bus watcher request and wait for the server to unblock.
- cancelWatcher()
- server.BlockWhileInUse(ctx)
- // The current user should not be set after a disconnect, as no one is
- // currently using the server.
- server.CheckCurrentUser(nil)
- }
- func TestIPNAlreadyInUseOnWindows(t *testing.T) {
- enableLogging := false
- setGOOSForTest(t, "windows")
- ctx := context.Background()
- server := lapitest.NewServer(t, lapitest.WithLogging(enableLogging))
- // UserA connects and starts watching the IPN bus.
- clientA := server.ClientWithName("UserA")
- clientA.WatchIPNBus(ctx, 0)
- // While UserA is connected, UserB should not be able to connect.
- clientB := server.ClientWithName("UserB")
- if _, gotErr := clientB.Status(ctx); gotErr == nil {
- t.Fatalf("Status(%q): want error; got nil", clientB.Username())
- } else if wantError := "401 Unauthorized: Tailscale already in use by UserA"; gotErr.Error() != wantError {
- t.Fatalf("Status(%q): want %q; got %q", clientB.Username(), wantError, gotErr.Error())
- }
- // Current user should still be UserA.
- server.CheckCurrentUser(clientA.Actor)
- }
- func TestSequentialOSUserSwitchingOnWindows(t *testing.T) {
- enableLogging := false
- setGOOSForTest(t, "windows")
- ctx := context.Background()
- server := lapitest.NewServer(t, lapitest.WithLogging(enableLogging))
- connectDisconnectAsUser := func(name string) {
- // User connects and starts watching the IPN bus.
- client := server.ClientWithName(name)
- watcher, cancelWatcher := client.WatchIPNBus(ctx, 0)
- defer cancelWatcher()
- go pumpIPNBus(watcher)
- // It should be the current user from the LocalBackend's perspective...
- server.CheckCurrentUser(client.Actor)
- // until it disconnects.
- cancelWatcher()
- server.BlockWhileInUse(ctx)
- // Now, the current user should be unset.
- server.CheckCurrentUser(nil)
- }
- // UserA logs in, uses Tailscale for a bit, then logs out.
- connectDisconnectAsUser("UserA")
- // Same for UserB.
- connectDisconnectAsUser("UserB")
- }
- func TestConcurrentOSUserSwitchingOnWindows(t *testing.T) {
- enableLogging := false
- setGOOSForTest(t, "windows")
- ctx := context.Background()
- server := lapitest.NewServer(t, lapitest.WithLogging(enableLogging))
- connectDisconnectAsUser := func(name string) {
- // User connects and starts watching the IPN bus.
- client := server.ClientWithName(name)
- watcher, cancelWatcher := client.WatchIPNBus(ctx, ipn.NotifyInitialState)
- defer cancelWatcher()
- runtime.Gosched()
- // Get the current user from the LocalBackend's perspective
- // as soon as we're connected.
- gotUID, gotActor := server.Backend().CurrentUserForTest()
- // Wait for the first notification to arrive.
- // It will either be the initial state we've requested via [ipn.NotifyInitialState],
- // returned by an actual handler, or a "fake" notification sent by the server
- // itself to indicate that it is being used by someone else.
- n, err := watcher.Next()
- if err != nil {
- t.Fatal(err)
- }
- // If our user lost the race and the IPN is in use by another user,
- // we should just return. For the sake of this test, we're not
- // interested in waiting for the server to become idle.
- if n.State != nil && *n.State == ipn.InUseOtherUser {
- return
- }
- // Otherwise, our user should have been the current user since the time we connected.
- if gotUID != client.Actor.UserID() {
- t.Errorf("CurrentUser(Initial): got UID %q; want %q", gotUID, client.Actor.UserID())
- return
- }
- if hasActor := gotActor != nil; !hasActor || gotActor != client.Actor {
- t.Errorf("CurrentUser(Initial): got %v; want %v", gotActor, client.Actor)
- return
- }
- // And should still be the current user (as they're still connected)...
- server.CheckCurrentUser(client.Actor)
- }
- numIterations := 10
- for range numIterations {
- numGoRoutines := 100
- var wg sync.WaitGroup
- wg.Add(numGoRoutines)
- for i := range numGoRoutines {
- // User logs in, uses Tailscale for a bit, then logs out
- // in parallel with other users doing the same.
- go func() {
- defer wg.Done()
- connectDisconnectAsUser("User-" + strconv.Itoa(i))
- }()
- }
- wg.Wait()
- if err := server.BlockWhileInUse(ctx); err != nil {
- t.Fatalf("BlockUntilIdle: %v", err)
- }
- server.CheckCurrentUser(nil)
- }
- }
- func TestBlockWhileIdentityInUse(t *testing.T) {
- enableLogging := false
- setGOOSForTest(t, "windows")
- ctx := context.Background()
- server := lapitest.NewServer(t, lapitest.WithLogging(enableLogging))
- // connectWaitDisconnectAsUser connects as a user with the specified name
- // and keeps the IPN bus watcher alive until the context is canceled.
- // It returns a channel that is closed when done.
- connectWaitDisconnectAsUser := func(ctx context.Context, name string) <-chan struct{} {
- client := server.ClientWithName(name)
- watcher, cancelWatcher := client.WatchIPNBus(ctx, 0)
- done := make(chan struct{})
- go func() {
- defer cancelWatcher()
- defer close(done)
- for {
- _, err := watcher.Next()
- if err != nil {
- // There's either an error or the request has been canceled.
- break
- }
- }
- }()
- return done
- }
- for range 100 {
- // Connect as UserA, and keep the connection alive
- // until disconnectUserA is called.
- userAContext, disconnectUserA := context.WithCancel(ctx)
- userADone := connectWaitDisconnectAsUser(userAContext, "UserA")
- disconnectUserA()
- // Check if userB can connect. Calling it directly increases
- // the likelihood of triggering a deadlock due to a race condition
- // in blockWhileIdentityInUse. But the issue also occurs during
- // the normal execution path when UserB connects to the IPN server
- // while UserA is disconnecting.
- userB := server.MakeTestActor("UserB", "ClientB")
- server.BlockWhileInUseByOther(ctx, userB)
- <-userADone
- }
- }
- func TestShutdownViaLocalAPI(t *testing.T) {
- t.Parallel()
- errAccessDeniedByPolicy := errors.New("Access denied: shutdown access denied by policy")
- tests := []struct {
- name string
- allowTailscaledRestart *bool
- wantErr error
- }{
- {
- name: "AllowTailscaledRestart/NotConfigured",
- allowTailscaledRestart: nil,
- wantErr: errAccessDeniedByPolicy,
- },
- {
- name: "AllowTailscaledRestart/False",
- allowTailscaledRestart: ptr.To(false),
- wantErr: errAccessDeniedByPolicy,
- },
- {
- name: "AllowTailscaledRestart/True",
- allowTailscaledRestart: ptr.To(true),
- wantErr: nil, // shutdown should be allowed
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- sys := tsd.NewSystem()
- var pol policytest.Config
- if tt.allowTailscaledRestart != nil {
- pol.Set(pkey.AllowTailscaledRestart, *tt.allowTailscaledRestart)
- }
- sys.Set(pol)
- server := lapitest.NewServer(t, lapitest.WithSys(sys))
- lc := server.ClientWithName("User")
- err := lc.ShutdownTailscaled(t.Context())
- checkError(t, err, tt.wantErr)
- })
- }
- }
- func checkError(tb testing.TB, got, want error) {
- tb.Helper()
- if (want == nil) != (got == nil) ||
- (want != nil && got != nil && want.Error() != got.Error() && !errors.Is(got, want)) {
- tb.Fatalf("gotErr: %v; wantErr: %v", got, want)
- }
- }
- func setGOOSForTest(tb testing.TB, goos string) {
- tb.Helper()
- envknob.Setenv("TS_DEBUG_FAKE_GOOS", goos)
- tb.Cleanup(func() { envknob.Setenv("TS_DEBUG_FAKE_GOOS", "") })
- }
- func pumpIPNBus(watcher *local.IPNBusWatcher) {
- for {
- _, err := watcher.Next()
- if err != nil {
- break
- }
- }
- }
|