From 317201375f92933d43bba86ee8d3590f5e54ab8d Mon Sep 17 00:00:00 2001 From: Gesa Stupperich Date: Wed, 3 Jun 2026 12:44:52 +0100 Subject: [PATCH] tsnet: test key extension after server restart Updates #19326 Signed-off-by: Gesa Stupperich --- tsnet/tsnet_test.go | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index d6c125b50..6d100ae03 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -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()) + } +}