mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-20 11:58:39 +00:00
ipn/ipnserver: fix race condition where LocalBackend is reset after a different user connects
In this commit, we add a failing test to verify that ipn/ipnserver.Server correctly sets and unsets the current user when two different clients send requests concurrently (A sends request, B sends request, A's request completes, B's request completes). The expectation is that the user who wins the race becomes the current user from the LocalBackend's perspective, remaining in this state until they disconnect, after which a different user should be able to connect and use the LocalBackend. We then fix the second of two bugs in (*Server).addActiveHTTPRequest, where a race condition causes the LocalBackend's state to be reset after a new client connects, instead of after the last active request of the previous client completes and the server becomes idle. Fixes tailscale/corp#25804 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
f33f5f99c0
commit
6fac2903e1
@ -394,11 +394,14 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, actor ipnauth.Actor) (o
|
||||
|
||||
onDone = func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.activeReqs, req)
|
||||
remain := len(s.activeReqs)
|
||||
s.mu.Unlock()
|
||||
if len(s.activeReqs) != 0 {
|
||||
// The server is not idle yet.
|
||||
return
|
||||
}
|
||||
|
||||
if remain == 0 && s.resetOnZero {
|
||||
if s.resetOnZero {
|
||||
if lb.InServerMode() {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
@ -408,11 +411,7 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, actor ipnauth.Actor) (o
|
||||
}
|
||||
|
||||
// Wake up callers waiting for the server to be idle:
|
||||
if remain == 0 {
|
||||
s.mu.Lock()
|
||||
s.zeroReqWaiter.wakeAll()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.zeroReqWaiter.wakeAll()
|
||||
}
|
||||
|
||||
return onDone, nil
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@ -187,6 +188,72 @@ func TestSequentialOSUserSwitchingOnWindows(t *testing.T) {
|
||||
connectDisconnectAsUser("UserB")
|
||||
}
|
||||
|
||||
func TestConcurrentOSUserSwitchingOnWindows(t *testing.T) {
|
||||
enableLogging := false
|
||||
setGOOSForTest(t, "windows")
|
||||
|
||||
ctx := context.Background()
|
||||
server := startDefaultTestIPNServer(t, ctx, enableLogging)
|
||||
|
||||
connectDisconnectAsUser := func(name string) {
|
||||
// User connects and starts watching the IPN bus.
|
||||
client := server.getClientAs(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.mustBackend().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.User.UID {
|
||||
t.Errorf("CurrentUser(Initial): got UID %q; want %q", gotUID, client.User.UID)
|
||||
return
|
||||
}
|
||||
if gotActor, ok := gotActor.(*ipnauth.TestActor); !ok || *gotActor != *client.User {
|
||||
t.Errorf("CurrentUser(Initial): got %v; want %v", gotActor, client.User)
|
||||
return
|
||||
}
|
||||
|
||||
// And should still be the current user (as they're still connected)...
|
||||
server.checkCurrentUser(client.User)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func setGOOSForTest(tb testing.TB, goos string) {
|
||||
tb.Helper()
|
||||
envknob.Setenv("TS_DEBUG_FAKE_GOOS", goos)
|
||||
|
Loading…
x
Reference in New Issue
Block a user