mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-21 12:28:39 +00:00

tailscaled's ipn package writes a collection of keys to state after authenticating to control, but one at a time. If containerboot happens to send a SIGTERM signal to tailscaled in the middle of writing those keys, it may shut down with an inconsistent state Secret and never recover. While we can't durably fix this with our current single-use auth keys (no atomic operation to auth + write state), we can reduce the window for this race condition by checking for partial state before sending SIGTERM to tailscaled. Best effort only. Updates #14080 Change-Id: I0532d51b6f0b7d391e538468bd6a0a80dbe1d9f7 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
241 lines
6.7 KiB
Go
241 lines
6.7 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/kube/kubeapi"
|
|
"tailscale.com/kube/kubeclient"
|
|
)
|
|
|
|
func TestSetupKube(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg *settings
|
|
wantErr bool
|
|
wantCfg *settings
|
|
kc *kubeClient
|
|
}{
|
|
{
|
|
name: "TS_AUTHKEY set, state Secret exists",
|
|
cfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, false, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return nil, nil
|
|
},
|
|
}},
|
|
wantCfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
},
|
|
{
|
|
name: "TS_AUTHKEY set, state Secret does not exist, we have permissions to create it",
|
|
cfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, true, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return nil, &kubeapi.Status{Code: 404}
|
|
},
|
|
}},
|
|
wantCfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
},
|
|
{
|
|
name: "TS_AUTHKEY set, state Secret does not exist, we do not have permissions to create it",
|
|
cfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, false, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return nil, &kubeapi.Status{Code: 404}
|
|
},
|
|
}},
|
|
wantCfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "TS_AUTHKEY set, we encounter a non-404 error when trying to retrieve the state Secret",
|
|
cfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, false, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return nil, &kubeapi.Status{Code: 403}
|
|
},
|
|
}},
|
|
wantCfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "TS_AUTHKEY set, we encounter a non-404 error when trying to check Secret permissions",
|
|
cfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
wantCfg: &settings{
|
|
AuthKey: "foo",
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, false, errors.New("broken")
|
|
},
|
|
}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
// Interactive login using URL in Pod logs
|
|
name: "TS_AUTHKEY not set, state Secret does not exist, we have permissions to create it",
|
|
cfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
wantCfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, true, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return nil, &kubeapi.Status{Code: 404}
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
// Interactive login using URL in Pod logs
|
|
name: "TS_AUTHKEY not set, state Secret exists, but does not contain auth key",
|
|
cfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
wantCfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, false, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return &kubeapi.Secret{}, nil
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "TS_AUTHKEY not set, state Secret contains auth key, we do not have RBAC to patch it",
|
|
cfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return false, false, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return &kubeapi.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil
|
|
},
|
|
}},
|
|
wantCfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "TS_AUTHKEY not set, state Secret contains auth key, we have RBAC to patch it",
|
|
cfg: &settings{
|
|
KubeSecret: "foo",
|
|
},
|
|
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
|
|
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
|
|
return true, false, nil
|
|
},
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return &kubeapi.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil
|
|
},
|
|
}},
|
|
wantCfg: &settings{
|
|
KubeSecret: "foo",
|
|
AuthKey: "foo",
|
|
KubernetesCanPatch: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
kc := tt.kc
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if err := tt.cfg.setupKube(context.Background(), kc); (err != nil) != tt.wantErr {
|
|
t.Errorf("settings.setupKube() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if diff := cmp.Diff(*tt.cfg, *tt.wantCfg); diff != "" {
|
|
t.Errorf("unexpected contents of settings after running settings.setupKube()\n(-got +want):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWaitForConsistentState(t *testing.T) {
|
|
data := map[string][]byte{
|
|
// Missing _current-profile.
|
|
string(ipn.KnownProfilesStateKey): []byte(""),
|
|
string(ipn.MachineKeyStateKey): []byte(""),
|
|
"profile-foo": []byte(""),
|
|
}
|
|
kc := &kubeClient{
|
|
Client: &kubeclient.FakeClient{
|
|
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
|
|
return &kubeapi.Secret{
|
|
Data: data,
|
|
}, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
if err := kc.waitForConsistentState(ctx); err != context.DeadlineExceeded {
|
|
t.Fatalf("expected DeadlineExceeded, got %v", err)
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
data[string(ipn.CurrentProfileStateKey)] = []byte("")
|
|
if err := kc.waitForConsistentState(ctx); err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
}
|