Revert "control/controlclient: continue map poll during key expiry to receive extensions" (#20257)
* Revert "control/controlclient: continue map poll during key expiry to receive extensions" This reverts commit6a822dcc36. This commit has caused test failures in the corp repo by unexpected changing the login behaviour when nodes have a valid node key. Updates tailscale/corp#43705 Updates #19326 Signed-off-by: Alex Chan <alexc@tailscale.com> * Revert "tsnet: test key extension after server restart" This reverts commit317201375f. This test relies on changes in317201375f, which is also being reverted because it causes test failures in corp. Updates tailscale/corp#43705 Updates #19326 Signed-off-by: Alex Chan <alexc@tailscale.com> --------- Signed-off-by: Alex Chan <alexc@tailscale.com>
This commit is contained in:
@@ -1,449 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
// TestKeyExtensionWakesUpExpiredClient verifies that when a client is in NeedsLogin
|
||||
// state due to key expiry, receiving a netmap with an extended (future) KeyExpiry
|
||||
// correctly transitions the client back to a working state.
|
||||
//
|
||||
// This tests the key recovery path: client has expired key -> admin extends key
|
||||
// -> server sends updated netmap -> client should recover.
|
||||
func TestKeyExtensionWakesUpExpiredClient(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
|
||||
// Setup test infrastructure
|
||||
sys := tsd.NewSystem()
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(e.Close)
|
||||
sys.Set(e)
|
||||
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(b.Shutdown)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
// Start the backend
|
||||
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||
|
||||
// Simulate successful login and authenticated state
|
||||
cc.populateKeys()
|
||||
nodeKey := key.NewNode().Public()
|
||||
now := time.Now()
|
||||
|
||||
// First, get to a Running state with a valid key
|
||||
futureExpiry := now.Add(1 * time.Hour)
|
||||
cc.send(sendOpt{
|
||||
loginFinished: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: futureExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Enable WantRunning - required for keyExpired to trigger NeedsLogin state
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
})
|
||||
|
||||
// Verify we're in a good state initially
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsFalse, qt.Commentf("key should not be expired initially"))
|
||||
b.mu.Unlock()
|
||||
|
||||
// Now simulate key expiry by sending a netmap with past KeyExpiry
|
||||
pastExpiry := now.Add(-1 * time.Hour)
|
||||
cc.send(sendOpt{
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: pastExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Verify the client detects key expiry
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsTrue, qt.Commentf("key should be detected as expired"))
|
||||
b.mu.Unlock()
|
||||
|
||||
// Verify state is NeedsLogin (requires WantRunning=true)
|
||||
state := b.State()
|
||||
c.Assert(state, qt.Equals, ipn.NeedsLogin, qt.Commentf("state should be NeedsLogin when key is expired and WantRunning=true"))
|
||||
|
||||
// Set blocked to true to simulate the engine being blocked (as would happen
|
||||
// when entering NeedsLogin due to key expiry in real flow)
|
||||
b.mu.Lock()
|
||||
b.blocked = true
|
||||
b.mu.Unlock()
|
||||
|
||||
// Now simulate admin extending the key - server sends new netmap with extended expiry
|
||||
extendedExpiry := now.Add(30 * time.Minute)
|
||||
cc.send(sendOpt{
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: extendedExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Verify the client recovers:
|
||||
// 1. keyExpired should be false
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsFalse, qt.Commentf("key should no longer be expired after extension"))
|
||||
|
||||
// 2. blocked should be false (unblocked when key extended)
|
||||
c.Assert(b.blocked, qt.IsFalse, qt.Commentf("engine should be unblocked after key extension"))
|
||||
b.mu.Unlock()
|
||||
|
||||
// 3. state should transition away from NeedsLogin
|
||||
// Note: exact state depends on other factors (MachineAuthorized, etc.)
|
||||
// but it should NOT be NeedsLogin anymore
|
||||
state = b.State()
|
||||
if state == ipn.NeedsLogin {
|
||||
// Check if it's still NeedsLogin for a reason OTHER than key expiry
|
||||
b.mu.Lock()
|
||||
keyExp := b.keyExpired
|
||||
b.mu.Unlock()
|
||||
if !keyExp {
|
||||
// Key is not expired, so NeedsLogin must be for another reason
|
||||
// (which is acceptable in this test's context)
|
||||
t.Logf("state is NeedsLogin but keyExpired=false, which is acceptable")
|
||||
} else {
|
||||
t.Errorf("state is still NeedsLogin with keyExpired=true after key extension")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeyExpiredStateMachine verifies that when a key expires, the state machine
|
||||
// correctly transitions to NeedsLogin and sets keyExpired=true.
|
||||
func TestKeyExpiredStateMachine(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(e.Close)
|
||||
sys.Set(e)
|
||||
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(b.Shutdown)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||
|
||||
cc.populateKeys()
|
||||
nodeKey := key.NewNode().Public()
|
||||
now := time.Now()
|
||||
|
||||
// Get to Running state with valid key
|
||||
futureExpiry := now.Add(1 * time.Hour)
|
||||
cc.send(sendOpt{
|
||||
loginFinished: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: futureExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Enable WantRunning
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
})
|
||||
|
||||
// Verify initial state
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsFalse)
|
||||
b.mu.Unlock()
|
||||
|
||||
// Now expire the key
|
||||
pastExpiry := now.Add(-1 * time.Hour)
|
||||
cc.send(sendOpt{
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: pastExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Verify keyExpired is set
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsTrue, qt.Commentf("keyExpired should be true after receiving expired KeyExpiry"))
|
||||
b.mu.Unlock()
|
||||
|
||||
// Verify state is NeedsLogin
|
||||
c.Assert(b.State(), qt.Equals, ipn.NeedsLogin)
|
||||
}
|
||||
|
||||
// TestKeyExpiryExtendedUnblocksEngine verifies that when a key is extended,
|
||||
// the engine is unblocked even if it was blocked due to key expiry.
|
||||
func TestKeyExpiryExtendedUnblocksEngine(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(e.Close)
|
||||
sys.Set(e)
|
||||
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(b.Shutdown)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||
|
||||
cc.populateKeys()
|
||||
nodeKey := key.NewNode().Public()
|
||||
now := time.Now()
|
||||
|
||||
// Get to authenticated state
|
||||
cc.send(sendOpt{
|
||||
loginFinished: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: now.Add(1 * time.Hour),
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Simulate key expiry
|
||||
pastExpiry := now.Add(-1 * time.Hour)
|
||||
cc.send(sendOpt{
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: pastExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Manually set blocked=true to simulate blocked state
|
||||
b.mu.Lock()
|
||||
b.blocked = true
|
||||
wasBlocked := b.blocked
|
||||
b.mu.Unlock()
|
||||
c.Assert(wasBlocked, qt.IsTrue)
|
||||
|
||||
// Extend the key
|
||||
extendedExpiry := now.Add(30 * time.Minute)
|
||||
cc.send(sendOpt{
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: extendedExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Verify engine is unblocked
|
||||
b.mu.Lock()
|
||||
c.Assert(b.blocked, qt.IsFalse, qt.Commentf("engine should be unblocked after key extension"))
|
||||
c.Assert(b.keyExpired, qt.IsFalse, qt.Commentf("keyExpired should be false after extension"))
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
// TestKeyExpiryZeroMeansNoExpiry verifies that a zero KeyExpiry (used for
|
||||
// tagged nodes or nodes with expiry disabled) is not treated as expired.
|
||||
func TestKeyExpiryZeroMeansNoExpiry(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(e.Close)
|
||||
sys.Set(e)
|
||||
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(b.Shutdown)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||
|
||||
cc.populateKeys()
|
||||
nodeKey := key.NewNode().Public()
|
||||
|
||||
// Send netmap with zero KeyExpiry (like a tagged node)
|
||||
cc.send(sendOpt{
|
||||
loginFinished: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: time.Time{}, // zero = no expiry
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
// Verify key is NOT considered expired
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsFalse, qt.Commentf("zero KeyExpiry should not be treated as expired"))
|
||||
b.mu.Unlock()
|
||||
|
||||
// State should not be NeedsLogin due to expiry
|
||||
state := b.State()
|
||||
c.Assert(state, qt.Not(qt.Equals), ipn.NeedsLogin, qt.Commentf("should not be in NeedsLogin with zero KeyExpiry"))
|
||||
}
|
||||
|
||||
// TestKeyExpiryWithNetMapUpdate verifies that key expiry detection works
|
||||
// correctly across multiple netmap updates with varying expiry times.
|
||||
func TestKeyExpiryWithNetMapUpdate(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(e.Close)
|
||||
sys.Set(e)
|
||||
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(b.Shutdown)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||
|
||||
cc.populateKeys()
|
||||
nodeKey := key.NewNode().Public()
|
||||
now := time.Now()
|
||||
|
||||
// Initial login with future expiry
|
||||
futureExpiry := now.Add(24 * time.Hour)
|
||||
cc.send(sendOpt{
|
||||
loginFinished: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: futureExpiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
b.mu.Lock()
|
||||
c.Assert(b.keyExpired, qt.IsFalse)
|
||||
b.mu.Unlock()
|
||||
|
||||
// Simulate multiple netmap updates, tracking keyExpired state
|
||||
testCases := []struct {
|
||||
name string
|
||||
expiry time.Time
|
||||
wantExpired bool
|
||||
}{
|
||||
{"still valid", now.Add(12 * time.Hour), false},
|
||||
{"expires soon", now.Add(5 * time.Minute), false},
|
||||
{"just expired", now.Add(-1 * time.Second), true},
|
||||
{"expired long ago", now.Add(-24 * time.Hour), true},
|
||||
{"extended again", now.Add(1 * time.Hour), false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cc.send(sendOpt{
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Key: nodeKey,
|
||||
MachineAuthorized: true,
|
||||
KeyExpiry: tc.expiry,
|
||||
}).View(),
|
||||
},
|
||||
})
|
||||
|
||||
b.mu.Lock()
|
||||
gotExpired := b.keyExpired
|
||||
b.mu.Unlock()
|
||||
|
||||
c.Assert(gotExpired, qt.Equals, tc.wantExpired,
|
||||
qt.Commentf("%s: expiry=%v, keyExpired=%v, want=%v",
|
||||
tc.name, tc.expiry, gotExpired, tc.wantExpired))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user