ipn/store: automatically migrate between plaintext and encrypted state (#16318)

Add a new `--encrypt-state` flag to `cmd/tailscaled`. Based on that
flag, migrate the existing state file to/from encrypted format if
needed.

Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov
2025-06-26 17:09:13 -07:00
committed by GitHub
parent d2c1ed22c3
commit 6feb3c35cb
24 changed files with 546 additions and 26 deletions

View File

@@ -21,6 +21,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"sync/atomic"
"testing"
@@ -32,6 +33,7 @@ import (
"tailscale.com/client/tailscale"
"tailscale.com/clientupdate"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
@@ -1470,3 +1472,60 @@ func TestNetstackUDPLoopback(t *testing.T) {
d1.MustCleanShutdown(t)
}
func TestEncryptStateMigration(t *testing.T) {
if !hostinfo.New().TPM.Present() {
t.Skip("TPM not available")
}
if runtime.GOOS != "linux" && runtime.GOOS != "windows" {
t.Skip("--encrypt-state for tailscaled state not supported on this platform")
}
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n := NewTestNode(t, env)
runNode := func(t *testing.T, wantStateKeys []string) {
t.Helper()
// Run the node.
d := n.StartDaemon()
n.AwaitResponding()
n.MustUp()
n.AwaitRunning()
// Check the contents of the state file.
buf, err := os.ReadFile(n.stateFile)
if err != nil {
t.Fatalf("reading %q: %v", n.stateFile, err)
}
t.Logf("state file content:\n%s", buf)
var content map[string]any
if err := json.Unmarshal(buf, &content); err != nil {
t.Fatalf("parsing %q: %v", n.stateFile, err)
}
for _, k := range wantStateKeys {
if _, ok := content[k]; !ok {
t.Errorf("state file is missing key %q", k)
}
}
// Stop the node.
d.MustCleanShutdown(t)
}
wantPlaintextStateKeys := []string{"_machinekey", "_current-profile", "_profiles"}
wantEncryptedStateKeys := []string{"key", "nonce", "data"}
t.Run("regular-state", func(t *testing.T) {
n.encryptState = false
runNode(t, wantPlaintextStateKeys)
})
t.Run("migrate-to-encrypted", func(t *testing.T) {
n.encryptState = true
runNode(t, wantEncryptedStateKeys)
})
t.Run("migrate-to-plaintext", func(t *testing.T) {
n.encryptState = false
runNode(t, wantPlaintextStateKeys)
})
}