You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/ipn/store.go

241 lines
6.2 KiB
Go

// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sync"
"time"
"tailscale.com/atomicfile"
"tailscale.com/kube"
"tailscale.com/paths"
)
// ErrStateNotExist is returned by StateStore.ReadState when the
// requested state ID doesn't exist.
var ErrStateNotExist = errors.New("no state with given ID")
const (
// MachineKeyStateKey is the key under which we store the machine key,
// in its wgkey.Private.MarshalText representation.
MachineKeyStateKey = StateKey("_machinekey")
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
// loads on startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
GlobalDaemonStateKey = StateKey("_daemon")
// ServerModeStartKey's value, if non-empty, is the value of a
// StateKey containing the prefs to start with which to start the
// server.
//
// For example, the value might be "user-1234", meaning the
// the server should start with the Prefs JSON loaded from
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
)
// StateStore persists state, and produces it back on request.
type StateStore interface {
// ReadState returns the bytes associated with ID. Returns (nil,
// ErrStateNotExist) if the ID doesn't have associated state.
ReadState(id StateKey) ([]byte, error)
// WriteState saves bs as the state associated with ID.
WriteState(id StateKey, bs []byte) error
}
// KubeStore is a StateStore that uses a Kubernetes Secret for persistence.
type KubeStore struct {
client *kube.Client
secretName string
}
// NewKubeStore returns a new KubeStore that persists to the named secret.
func NewKubeStore(secretName string) (*KubeStore, error) {
c, err := kube.New()
if err != nil {
return nil, err
}
return &KubeStore{
client: c,
secretName: secretName,
}, nil
}
func (s *KubeStore) String() string { return "KubeStore" }
// ReadState implements the StateStore interface.
func (s *KubeStore) ReadState(id StateKey) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
secret, err := s.client.GetSecret(ctx, s.secretName)
if err != nil {
if st, ok := err.(*kube.Status); ok && st.Code == 404 {
return nil, ErrStateNotExist
}
return nil, err
}
b, ok := secret.Data[string(id)]
if !ok {
return nil, ErrStateNotExist
}
return b, nil
}
// WriteState implements the StateStore interface.
func (s *KubeStore) WriteState(id StateKey, bs []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
secret, err := s.client.GetSecret(ctx, s.secretName)
if err != nil {
if st, ok := err.(*kube.Status); ok && st.Code == 404 {
return s.client.CreateSecret(ctx, &kube.Secret{
TypeMeta: kube.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: kube.ObjectMeta{
Name: s.secretName,
},
Data: map[string][]byte{
string(id): bs,
},
})
}
return err
}
secret.Data[string(id)] = bs
if err := s.client.UpdateSecret(ctx, secret); err != nil {
return err
}
return err
}
// MemoryStore is a store that keeps state in memory only.
type MemoryStore struct {
mu sync.Mutex
cache map[StateKey][]byte
}
func (s *MemoryStore) String() string { return "MemoryStore" }
// ReadState implements the StateStore interface.
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
bs, ok := s.cache[id]
if !ok {
return nil, ErrStateNotExist
}
return bs, nil
}
// WriteState implements the StateStore interface.
func (s *MemoryStore) WriteState(id StateKey, bs []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.cache == nil {
s.cache = map[StateKey][]byte{}
}
s.cache[id] = append([]byte(nil), bs...)
return nil
}
// FileStore is a StateStore that uses a JSON file for persistence.
type FileStore struct {
path string
mu sync.RWMutex
cache map[StateKey][]byte
}
// Path returns the path that NewFileStore was called with.
func (s *FileStore) Path() string { return s.path }
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
// NewFileStore returns a new file store that persists to path.
func NewFileStore(path string) (*FileStore, error) {
// We unconditionally call this to ensure that our perms are correct
if err := paths.MkStateDir(filepath.Dir(path)); err != nil {
return nil, fmt.Errorf("creating state directory: %w", err)
}
bs, err := ioutil.ReadFile(path)
// Treat an empty file as a missing file.
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
if err == nil && len(bs) == 0 {
log.Printf("ipn.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
err = os.ErrNotExist
}
if err != nil {
if os.IsNotExist(err) {
// Write out an initial file, to verify that we can write
// to the path.
if err = atomicfile.WriteFile(path, []byte("{}"), 0600); err != nil {
return nil, err
}
return &FileStore{
path: path,
cache: map[StateKey][]byte{},
}, nil
}
return nil, err
}
ret := &FileStore{
path: path,
cache: map[StateKey][]byte{},
}
if err := json.Unmarshal(bs, &ret.cache); err != nil {
return nil, err
}
return ret, nil
}
// ReadState implements the StateStore interface.
func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
bs, ok := s.cache[id]
if !ok {
return nil, ErrStateNotExist
}
return bs, nil
}
// WriteState implements the StateStore interface.
func (s *FileStore) WriteState(id StateKey, bs []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if bytes.Equal(s.cache[id], bs) {
return nil
}
s.cache[id] = append([]byte(nil), bs...)
bs, err := json.MarshalIndent(s.cache, "", " ")
if err != nil {
return err
}
return atomicfile.WriteFile(s.path, bs, 0600)
}