From 5bc632271b90d4896e6681d0a12ed0918f85a2cb Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 3 Feb 2020 10:35:52 -0800 Subject: [PATCH] Introduce a state store to LocalBackend. The store is passed-in by callers of NewLocalBackend and ipnserver.Run, but currently all callers are hardcoded to an in-memory store. The store is unused. Signed-Off-By: David Anderson --- cmd/tailscale/ipn.go | 35 +++++++----- cmd/tailscaled/ipnd.go | 2 + ipn/backend.go | 41 ++++++++++++-- ipn/e2e_test.go | 2 +- ipn/ipnserver/server.go | 13 ++++- ipn/local.go | 73 ++++++++++++++++++++++-- ipn/prefs.go | 3 + ipn/store.go | 117 ++++++++++++++++++++++++++++++++++++++ ipn/store_test.go | 121 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 382 insertions(+), 25 deletions(-) create mode 100644 ipn/store.go create mode 100644 ipn/store_test.go diff --git a/cmd/tailscale/ipn.go b/cmd/tailscale/ipn.go index a957edb8f..397f5114a 100644 --- a/cmd/tailscale/ipn.go +++ b/cmd/tailscale/ipn.go @@ -44,14 +44,15 @@ func main() { log.Printf("fixConsoleOutput: %v\n", err) } config := getopt.StringLong("config", 'f', "", "path to config file") + statekey := getopt.StringLong("statekey", 0, "", "state key for daemon-side config") server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server") nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes") rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes") droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node") getopt.Parse() - if *config == "" { + if *config == "" && *statekey == "" { logpolicy.New("tailnode.log.tailscale.io", "tailscale") - log.Fatal("no --config file specified") + log.Fatal("no --config or --statekey provided") } if len(getopt.Args()) > 0 { log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0]) @@ -60,16 +61,20 @@ func main() { pol := logpolicy.New("tailnode.log.tailscale.io", *config) defer pol.Close() - prefs, err := loadConfig(*config) - if err != nil { - log.Fatal(err) - } + var prefs *ipn.Prefs + if *config != "" { + localCfg, err := loadConfig(*config) + if err != nil { + log.Fatal(err) + } - // TODO(apenwarr): fix different semantics between prefs and uflags - // TODO(apenwarr): allow setting/using CorpDNS - prefs.WantRunning = true - prefs.RouteAll = *rroutes || *droutes - prefs.AllowSingleHosts = !*nuroutes + // TODO(apenwarr): fix different semantics between prefs and uflags + // TODO(apenwarr): allow setting/using CorpDNS + prefs = &localCfg + prefs.WantRunning = true + prefs.RouteAll = *rroutes || *droutes + prefs.AllowSingleHosts = !*nuroutes + } c, err := safesocket.Connect("", "Tailscale", "tailscaled", 41112) if err != nil { @@ -90,7 +95,8 @@ func main() { bc := ipn.NewBackendClient(log.Printf, clientToServer) opts := ipn.Options{ - Prefs: &prefs, + StateKey: ipn.StateKey(*statekey), + Prefs: prefs, ServerURL: *server, Notify: func(n ipn.Notify) { log.Printf("Notify: %v\n", n) @@ -112,7 +118,7 @@ func main() { fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url) } if p := n.Prefs; p != nil { - prefs = *p + prefs = p saveConfig(*config, *p) } }, @@ -131,6 +137,9 @@ func loadConfig(path string) (ipn.Prefs, error) { } func saveConfig(path string, prefs ipn.Prefs) error { + if path == "" { + return nil + } b, err := json.MarshalIndent(prefs, "", "\t") if err != nil { return fmt.Errorf("save config: %v", err) diff --git a/cmd/tailscaled/ipnd.go b/cmd/tailscaled/ipnd.go index 8d2b68b77..502f5ce9d 100644 --- a/cmd/tailscaled/ipnd.go +++ b/cmd/tailscaled/ipnd.go @@ -28,6 +28,7 @@ func main() { debug := getopt.StringLong("debug", 0, "", "Address of debug server") tunname := getopt.StringLong("tun", 0, "ts0", "tunnel interface name") listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)") + statepath := getopt.StringLong("state", 0, "", "Path of state file") logf := wgengine.RusagePrefixLog(log.Printf) @@ -58,6 +59,7 @@ func main() { e = wgengine.NewWatchdog(e) opts := ipnserver.Options{ + StatePath: *statepath, SurviveDisconnects: true, AllowQuit: false, } diff --git a/ipn/backend.go b/ipn/backend.go index b69871477..5fbd7d8e7 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -50,11 +50,44 @@ type Notify struct { BackendLogID *string // public logtail id used by backend } +// StateKey is an opaque identifier for a set of LocalBackend state +// (preferences, private keys, etc.). +// +// The reason we need this is that the Tailscale agent may be running +// on a multi-user machine, in a context where a single daemon is +// shared by several consecutive users. Ideally we would just use the +// username of the connected frontend as the StateKey. +// +// However, on Windows, there seems to be no safe way to figure out +// the owning user of a process connected over IPC mechanisms +// (sockets, named pipes). So instead, on Windows, we use a +// capability-oriented system where the frontend generates a random +// identifier for itself, and uses that as the StateKey when talking +// to the backend. That way, while we can't identify an OS user by +// name, we can tell two different users apart, because they'll have +// different opaque state keys (and no access to each others's keys). +// +// It would be much nicer if we could just figure out the OS user that +// owns the connected frontend, but here we are. +type StateKey string + type Options struct { - FrontendLogID string // public logtail id used by frontend - ServerURL string - Prefs *Prefs - Notify func(n Notify) `json:"-"` + // Public logtail id used by frontend. + FrontendLogID string + // Base URL for the tailcontrol server to talk to. + ServerURL string + // StateKey and Prefs together define the state the backend should + // use: + // - StateKey=="" && Prefs!=nil: use Prefs for internal state, + // don't persist changes in the backend. + // - StateKey!="" && Prefs==nil: load the given backend-side + // state and use/update that. + // - StateKey!="" && Prefs!=nil: like the previous case, but do + // an initial overwrite of backend state with Prefs. + StateKey StateKey + Prefs *Prefs + // Callback for backend events. + Notify func(Notify) `json:"-"` } type Backend interface { diff --git a/ipn/e2e_test.go b/ipn/e2e_test.go index 9b9b4de1b..ac91c36f2 100644 --- a/ipn/e2e_test.go +++ b/ipn/e2e_test.go @@ -158,7 +158,7 @@ func newNode(t *testing.T, prefix string, https *httptest.Server) testNode { if err != nil { t.Fatalf("NewFakeEngine: %v\n", err) } - n, err := NewLocalBackend(logf, prefix, e1) + n, err := NewLocalBackend(logf, prefix, &MemoryStore{}, e1) if err != nil { t.Fatalf("NewLocalBackend: %v\n", err) } diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index d8ee50ee9..e82086c0b 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -29,6 +29,7 @@ ) type Options struct { + StatePath string SurviveDisconnects bool AllowQuit bool } @@ -58,7 +59,17 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w return fmt.Errorf("safesocket.Listen: %v", err) } - b, err := ipn.NewLocalBackend(logf, logid, e) + var store ipn.StateStore + if opts.StatePath != "" { + store, err = ipn.NewFileStore(opts.StatePath) + if err != nil { + return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err) + } + } else { + store = &ipn.MemoryStore{} + } + + b, err := ipn.NewLocalBackend(logf, logid, store, e) if err != nil { return fmt.Errorf("NewLocalBackend: %v", err) } diff --git a/ipn/local.go b/ipn/local.go index 4fbfe1a35..9f88c791c 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -5,6 +5,7 @@ package ipn import ( + "errors" "fmt" "log" "strings" @@ -28,6 +29,7 @@ type LocalBackend struct { notify func(n Notify) c *controlclient.Client e wgengine.Engine + store StateStore serverURL string backendLogID string portpoll *portlist.Poller // may be nil @@ -36,6 +38,7 @@ type LocalBackend struct { // The mutex protects the following elements. mu sync.Mutex + stateKey StateKey prefs Prefs state State hiCache tailcfg.Hostinfo @@ -52,7 +55,9 @@ type LocalBackend struct { statusChanged *sync.Cond } -func NewLocalBackend(logf logger.Logf, logid string, e wgengine.Engine) (*LocalBackend, error) { +// NewLocalBackend returns a new LocalBackend that is ready to run, +// but is not actually running. +func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengine.Engine) (*LocalBackend, error) { if e == nil { panic("ipn.NewLocalBackend: wgengine must not be nil") } @@ -68,6 +73,7 @@ func NewLocalBackend(logf logger.Logf, logid string, e wgengine.Engine) (*LocalB b := LocalBackend{ logf: logf, e: e, + store: store, backendLogID: logid, state: NoState, portpoll: portpoll, @@ -113,8 +119,8 @@ func (b *LocalBackend) SetCmpDiff(cmpDiff func(x, y interface{}) string) { } func (b *LocalBackend) Start(opts Options) error { - if opts.Prefs == nil { - panic("Prefs can't be nil yet") + if opts.Prefs == nil && opts.StateKey == "" { + return errors.New("no state key or prefs provided") } if b.c != nil { @@ -128,7 +134,11 @@ func (b *LocalBackend) Start(opts Options) error { b.c.Shutdown() } - b.logf("Start: %v\n", opts.Prefs.Pretty()) + if opts.Prefs != nil { + b.logf("Start: %v\n", opts.Prefs.Pretty()) + } else { + b.logf("Start\n") + } hi := controlclient.NewHostinfo() hi.BackendLogID = b.backendLogID @@ -139,9 +149,12 @@ func (b *LocalBackend) Start(opts Options) error { b.hiCache = hi b.state = NoState b.serverURL = opts.ServerURL - if opts.Prefs != nil { - b.prefs = *opts.Prefs + + if err := b.loadStateWithLock(opts.StateKey, opts.Prefs); err != nil { + b.mu.Unlock() + return fmt.Errorf("loading requested state: %v", err) } + b.notify = opts.Notify b.netMapCache = nil b.mu.Unlock() @@ -187,6 +200,11 @@ func (b *LocalBackend) Start(opts Options) error { if new.Persist != nil { persist := *new.Persist // copy b.prefs.Persist = &persist + if b.stateKey != "" { + if err := b.store.WriteState(b.stateKey, b.prefs.ToBytes()); err != nil { + b.logf("Failed to save new controlclient state: %v", err) + } + } np := b.prefs b.send(Notify{Prefs: &np}) } @@ -257,6 +275,8 @@ func (b *LocalBackend) Start(opts Options) error { blid := b.backendLogID b.logf("Backend: logs: be:%v fe:%v\n", blid, opts.FrontendLogID) b.send(Notify{BackendLogID: &blid}) + nprefs := b.prefs // make a copy + b.send(Notify{Prefs: &nprefs}) cli.Login(nil, controlclient.LoginDefault) return nil @@ -338,6 +358,42 @@ func (b *LocalBackend) popBrowserAuthNow() { } } +func (b *LocalBackend) loadStateWithLock(key StateKey, prefs *Prefs) error { + switch { + case key != "" && prefs != nil: + b.logf("Importing frontend prefs into backend store") + if err := b.store.WriteState(key, prefs.ToBytes()); err != nil { + return fmt.Errorf("store.WriteState: %v", err) + } + fallthrough + case key != "": + b.logf("Using backend prefs") + bs, err := b.store.ReadState(key) + if err != nil { + if err == ErrStateNotExist { + b.prefs = NewPrefs() + b.stateKey = key + b.logf("Created empty state for %q", key) + return nil + } + return fmt.Errorf("store.ReadState(%q): %v", key, err) + } + b.prefs, err = PrefsFromBytes(bs, false) + if err != nil { + return fmt.Errorf("PrefsFromBytes: %v", err) + } + b.stateKey = key + case prefs != nil: + b.logf("Using frontend prefs") + b.prefs = *prefs + b.stateKey = "" + default: + panic("state key and prefs are unset") + } + + return nil +} + func (b *LocalBackend) State() State { b.mu.Lock() defer b.mu.Unlock() @@ -436,6 +492,11 @@ func (b *LocalBackend) SetPrefs(new Prefs) { old := b.prefs new.Persist = old.Persist // caller isn't allowed to override this b.prefs = new + if b.stateKey != "" { + if err := b.store.WriteState(b.stateKey, b.prefs.ToBytes()); err != nil { + b.logf("Failed to save new controlclient state: %v", err) + } + } b.mu.Unlock() if old.WantRunning != new.WantRunning { diff --git a/ipn/prefs.go b/ipn/prefs.go index 9fca69185..ae5a287cc 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -33,6 +33,9 @@ type Prefs struct { Persist *controlclient.Persist `json:"Config"` } +// IsEmpty reports whether p is nil or pointing to a Prefs zero value. +func (uc *Prefs) IsEmpty() bool { return uc == nil || *uc == Prefs{} } + func (uc *Prefs) Pretty() string { var ucp string if uc.Persist != nil { diff --git a/ipn/store.go b/ipn/store.go new file mode 100644 index 000000000..ab13262be --- /dev/null +++ b/ipn/store.go @@ -0,0 +1,117 @@ +// 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 ( + "encoding/json" + "errors" + "io/ioutil" + "os" + "sync" + + "tailscale.com/atomicfile" +) + +// ErrStateNotExist is returned by StateStore.ReadState when the +// requested state id doesn't exist. +var ErrStateNotExist = errors.New("no state with given id") + +// 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 +} + +// MemoryStore is a store that keeps state in memory only. +type MemoryStore struct { + mu sync.Mutex + cache map[StateKey][]byte +} + +func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.cache == nil { + s.cache = map[StateKey][]byte{} + } + bs, ok := s.cache[id] + if !ok { + return nil, ErrStateNotExist + } + return bs, nil +} + +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 +} + +// NewFileStore returns a new file store that persists to path. +func NewFileStore(path string) (*FileStore, error) { + bs, err := ioutil.ReadFile(path) + 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 returns the bytes persisted for id, if any. +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 persists bs under the key id. +func (s *FileStore) WriteState(id StateKey, bs []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + 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) +} diff --git a/ipn/store_test.go b/ipn/store_test.go new file mode 100644 index 000000000..c1f52a725 --- /dev/null +++ b/ipn/store_test.go @@ -0,0 +1,121 @@ +// 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 ( + "io/ioutil" + "os" + "testing" +) + +func testStoreSemantics(t *testing.T, store StateStore) { + t.Helper() + + tests := []struct { + // if true, data is data to write. If false, data is expected + // output of read. + write bool + id StateKey + data string + // If write=false, true if we expect a not-exist error. + notExists bool + }{ + { + id: "foo", + notExists: true, + }, + { + write: true, + id: "foo", + data: "bar", + }, + { + id: "foo", + data: "bar", + }, + { + id: "baz", + notExists: true, + }, + { + write: true, + id: "baz", + data: "quux", + }, + { + id: "foo", + data: "bar", + }, + { + id: "baz", + data: "quux", + }, + } + + for _, test := range tests { + if test.write { + if err := store.WriteState(test.id, []byte(test.data)); err != nil { + t.Errorf("writing %q to %q: %v", test.data, test.id, err) + } + } else { + bs, err := store.ReadState(test.id) + if err != nil { + if test.notExists && err == ErrStateNotExist { + continue + } + t.Errorf("reading %q: %v", test.id, err) + continue + } + if string(bs) != test.data { + t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data) + } + } + } +} + +func TestMemoryStore(t *testing.T) { + store := &MemoryStore{} + testStoreSemantics(t, store) +} + +func TestFileStore(t *testing.T) { + f, err := ioutil.TempFile("", "test_ipn_store") + if err != nil { + t.Fatal(err) + } + path := f.Name() + f.Close() + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + + store, err := NewFileStore(path) + if err != nil { + t.Fatalf("creating file store failed: %v", err) + } + + testStoreSemantics(t, store) + + // Build a brand new file store and check that both IDs written + // above are still there. + store, err = NewFileStore(path) + if err != nil { + t.Fatalf("creating second file store failed: %v", err) + } + + expected := map[StateKey]string{ + "foo": "bar", + "baz": "quux", + } + for id, want := range expected { + bs, err := store.ReadState(id) + if err != nil { + t.Errorf("reading %q (2nd store): %v", id, err) + } + if string(bs) != want { + t.Errorf("reading %q (2nd store): got %q, want %q", id, string(bs), want) + } + } +}