mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-01 12:31:52 +00:00

In this PR, we extract the in-process LocalAPI client/server implementation from ipn/ipnserver/server_test.go into a new ipntest package to be used in high‑level black‑box tests, such as those for the tailscale CLI. Updates #15575 Signed-off-by: Nick Khyl <nickk@tailscale.com>
270 lines
8.3 KiB
Go
270 lines
8.3 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnserver_test
|
|
|
|
import (
|
|
"context"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
|
|
"tailscale.com/client/local"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/lapitest"
|
|
"tailscale.com/types/ptr"
|
|
)
|
|
|
|
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 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
|
|
}
|
|
}
|
|
}
|