tsnet: test key extension after server restart

Updates #19326

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
This commit is contained in:
Gesa Stupperich
2026-06-03 12:44:52 +01:00
committed by Gesa Stupperich
parent ec8ab870a4
commit 317201375f

View File

@@ -3406,3 +3406,132 @@ func TestListenMultipleEphemeralPorts(t *testing.T) {
testMultipleEphemeral(t, lt)
})
}
// TestKeyExtensionAfterRestart verifies that a tsnet client with an expired node key
// that has launched into an interactive login after a restart recovers when the old
// key gets extended.
//
// See https://github.com/tailscale/tailscale/issues/19326.
func TestKeyExtensionAfterRestart(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
controlURL, control := startControl(t)
control.RequireAuth = true
tmp := filepath.Join(t.TempDir(), "s1")
os.MkdirAll(tmp, 0755)
newServer := func() *Server {
s := &Server{
Dir: tmp,
ControlURL: controlURL,
Hostname: "s1",
Logf: tstest.WhileTestRunningLogger(t),
}
t.Cleanup(func() { s.Close() })
return s
}
// Start a node as tsnet instance s1.
s1 := newServer()
if err := s1.Start(); err != nil {
t.Fatalf("s1.Start: %v", err)
}
upErrCh := make(chan error, 1)
go func() { _, err := s1.Up(ctx); upErrCh <- err }()
var initialAuthURL string
if err := tstest.WaitFor(20*time.Second, func() error {
url := s1.lb.StatusWithoutPeers().AuthURL
if url == "" {
return errors.New("no AuthURL yet")
}
initialAuthURL = url
return nil
}); err != nil {
t.Fatalf("waiting for initial AuthURL: %v", err)
}
if !control.CompleteAuth(initialAuthURL) {
t.Fatal("failed to complete initial AuthURL")
}
select {
case err := <-upErrCh:
if err != nil {
t.Fatalf("s1.Up: %v", err)
}
case <-time.After(20 * time.Second):
t.Fatalf("timed out waiting for s1.Up to return, s1.lb.State()=%v", s1.lb.State())
}
nodePub := s1.lb.StatusWithoutPeers().Self.PublicKey
// Expire s1's node key.
serverNode := control.Node(nodePub)
if serverNode == nil {
t.Fatalf("node %v not in control", nodePub)
}
serverNode.KeyExpiry = time.Now().Add(-time.Minute)
control.UpdateNode(serverNode)
// Wait for s1 to transition away from the Running state.
if err := tstest.WaitFor(20*time.Second, func() error {
if got := s1.lb.State(); got == ipn.Running {
return errors.New("still Running")
}
return nil
}); err != nil {
t.Fatalf("waiting to transition away from Running: %v", err)
}
if err := s1.Close(); err != nil {
t.Fatalf("s1.Close: %v", err)
}
// Restart the node as tsnet instance s2.
s2 := newServer()
if err := s2.Start(); err != nil {
t.Fatalf("s2.Start: %v", err)
}
s2UpErrCh := make(chan error, 1)
go func() { _, err := s2.Up(ctx); s2UpErrCh <- err }()
// Wait for s2 to transition into the NeedsLogin state.
var secondAuthURL string
if err := tstest.WaitFor(20*time.Second, func() error {
u := s2.lb.StatusWithoutPeers().AuthURL
if u == "" {
return errors.New("no AuthURL yet")
}
secondAuthURL = u
return nil
}); err != nil {
t.Fatalf("waiting for s2 AuthURL: %v", err)
}
// We deliberately do not complete the auth.
_ = secondAuthURL
// Extend the old node key.
serverNode.KeyExpiry = time.Now().Add(24 * time.Hour)
control.UpdateNode(serverNode)
// Wait for s2 to receive the netmap with the key extension info
// and transition to Running.
if err := tstest.WaitFor(20*time.Second, func() error {
if got := s2.lb.State(); got != ipn.Running {
return fmt.Errorf("in state %v; want Running", got)
}
return nil
}); err != nil {
t.Fatalf("waiting to return to Running after key extension: %v", err)
}
select {
case err := <-s2UpErrCh:
if err != nil {
t.Fatalf("s2.Up: %v", err)
}
case <-time.After(20 * time.Second):
t.Fatalf("timed out waiting for s2.Up to return, s2.lb.State()=%v", s2.lb.State())
}
}