diff --git a/control/controlclient/client.go b/control/controlclient/client.go index b809f8192..ef5af68c6 100644 --- a/control/controlclient/client.go +++ b/control/controlclient/client.go @@ -14,12 +14,20 @@ import ( "tailscale.com/tailcfg" ) +// LoginFlags is a bitmask of options to change the behavior of Client.Login +// and LocalBackend. type LoginFlags int const ( LoginDefault = LoginFlags(0) LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh LoginEphemeral // set RegisterRequest.Ephemeral + + // LocalBackendStartKeyOSNeutral instructs NewLocalBackend to start the + // LocalBackend without any OS-dependent StateStore StartKey behavior. + // + // See https://github.com/tailscale/tailscale/issues/6973. + LocalBackendStartKeyOSNeutral ) // Client represents a client connection to the control server. diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index cab5002a1..1144d93bf 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -308,7 +308,11 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo dialer := sys.Dialer.Get() _ = sys.MagicSock.Get() // or panic - pm, err := newProfileManager(store, logf) + goos := envknob.GOOS() + if loginFlags&controlclient.LocalBackendStartKeyOSNeutral != 0 { + goos = "" + } + pm, err := newProfileManagerWithGOOS(store, logf, goos) if err != nil { return nil, err } diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 9e6d34f4c..a82a60722 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -542,7 +542,7 @@ func (s *Server) start() (reterr error) { if s.Ephemeral { loginFlags = controlclient.LoginEphemeral } - lb, err := ipnlocal.NewLocalBackend(logf, s.logid, sys, loginFlags) + lb, err := ipnlocal.NewLocalBackend(logf, s.logid, sys, loginFlags|controlclient.LocalBackendStartKeyOSNeutral) if err != nil { return fmt.Errorf("NewLocalBackend: %v", err) } diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index 0c215ee5b..981e8c9b2 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -23,6 +23,7 @@ import ( "net/netip" "os" "path/filepath" + "reflect" "strings" "sync" "testing" @@ -470,6 +471,57 @@ func TestListenerCleanup(t *testing.T) { } } +// tests https://github.com/tailscale/tailscale/issues/6973 -- that we can start a tsnet server, +// stop it, and restart it, even on Windows. +func TestStartStopStartGetsSameIP(t *testing.T) { + controlURL := startControl(t) + + tmp := t.TempDir() + tmps1 := filepath.Join(tmp, "s1") + os.MkdirAll(tmps1, 0755) + + newServer := func() *Server { + return &Server{ + Dir: tmps1, + ControlURL: controlURL, + Hostname: "s1", + Logf: logger.TestLogger(t), + } + } + s1 := newServer() + defer s1.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + s1status, err := s1.Up(ctx) + if err != nil { + t.Fatal(err) + } + + firstIPs := s1status.TailscaleIPs + t.Logf("IPs: %v", firstIPs) + + if err := s1.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + s2 := newServer() + defer s2.Close() + + s2status, err := s2.Up(ctx) + if err != nil { + t.Fatalf("second Up: %v", err) + } + + secondIPs := s2status.TailscaleIPs + t.Logf("IPs: %v", secondIPs) + + if !reflect.DeepEqual(firstIPs, secondIPs) { + t.Fatalf("got %v but later %v", firstIPs, secondIPs) + } +} + func TestFunnel(t *testing.T) { ctx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second) defer dialCancel()