ipn, ipnserver, cmd/tailscale: add "server mode" support on Windows

This partially (but not yet fully) migrates Windows to tailscaled's
StateStore storage system.

This adds a new bool Pref, ForceDaemon, defined as:

// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.

Then, when ForceDaemon becomes true, we now write use the StateStore
to track which user started it in server mode, and store their prefs
under that key.

The ipnserver validates the connections/identities and informs that
LocalBackend which userid is currently in charge.

The GUI can then enable/disable server mode at runtime, without using
the CLI.

But the "tailscale up" CLI was also fixed, so Windows users can use
authkeys or ACL tags, etc.

Updates #275
pull/792/head
Brad Fitzpatrick 4 years ago
parent d027cd81df
commit 515866d7c6

@ -56,7 +56,6 @@ func runDown(ctx context.Context, args []string) error {
} }
return return
} }
log.Printf("Notify: %#v", n)
}) })
bc.RequestStatus() bc.RequestStatus()

@ -206,6 +206,8 @@ func runUp(ctx context.Context, args []string) error {
prefs.AdvertiseTags = tags prefs.AdvertiseTags = tags
prefs.NoSNAT = !upArgs.snat prefs.NoSNAT = !upArgs.snat
prefs.Hostname = upArgs.hostname prefs.Hostname = upArgs.hostname
prefs.ForceDaemon = (runtime.GOOS == "windows")
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
switch upArgs.netfilterMode { switch upArgs.netfilterMode {
case "on": case "on":
@ -229,6 +231,7 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) } startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
bc.SetPrefs(prefs) bc.SetPrefs(prefs)
opts := ipn.Options{ opts := ipn.Options{
StateKey: ipn.GlobalDaemonStateKey, StateKey: ipn.GlobalDaemonStateKey,
AuthKey: upArgs.authKey, AuthKey: upArgs.authKey,
@ -258,6 +261,22 @@ func runUp(ctx context.Context, args []string) error {
} }
}, },
} }
// On Windows, we still run in mostly the "legacy" way that
// predated the server's StateStore. That is, we send an empty
// StateKey and send the prefs directly. Although the Windows
// supports server mode, though, the transition to StateStore
// is only half complete. Only server mode uses it, and the
// Windows service (~tailscaled) is the one that computes the
// StateKey based on the connection idenity. So for now, just
// do as the Windows GUI's always done:
if runtime.GOOS == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
opts.Prefs = prefs
}
// We still have to Start right now because it's the only way to // We still have to Start right now because it's the only way to
// set up notifications and whatnot. This causes a bunch of churn // set up notifications and whatnot. This causes a bunch of churn
// every time the CLI touches anything. // every time the CLI touches anything.

@ -85,7 +85,9 @@ type Notify struct {
// //
// * the macOS/iOS GUI apps set it to "ipn-go-bridge" // * the macOS/iOS GUI apps set it to "ipn-go-bridge"
// * the Android app sets it to "ipn-android" // * the Android app sets it to "ipn-android"
// * on Windows, it's always the the empty string // * on Windows, it's the empty string (in client mode) or, via
// LocalBackend.userID, a string like "user-$USER_ID" (used in
// server mode).
// * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey) // * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
type StateKey string type StateKey string

@ -18,6 +18,7 @@ import (
"os/signal" "os/signal"
"os/user" "os/user"
"runtime" "runtime"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -68,6 +69,12 @@ type Options struct {
// its existing state, and accepts new frontend connections. If // its existing state, and accepts new frontend connections. If
// false, the server dumps its state and becomes idle. // false, the server dumps its state and becomes idle.
// //
// This is effectively whether the platform is in "server
// mode" by default. On Linux, it's true; on Windows, it's
// false. But on some platforms (currently only Windows), the
// "server mode" can be overridden at runtime with a change in
// Prefs.ForceDaemon/WantRunning.
//
// To support CLI connections (notably, "tailscale status"), // To support CLI connections (notably, "tailscale status"),
// the actual definition of "disconnect" is when the // the actual definition of "disconnect" is when the
// connection count transitions from 1 to 0. // connection count transitions from 1 to 0.
@ -81,15 +88,23 @@ type Options struct {
// server is an IPN backend and its set of 0 or more active connections // server is an IPN backend and its set of 0 or more active connections
// talking to an IPN backend. // talking to an IPN backend.
type server struct { type server struct {
resetOnZero bool // call bs.Reset on transition from 1->0 connections b *ipn.LocalBackend
b *ipn.LocalBackend logf logger.Logf
// resetOnZero is whether to call bs.Reset on transition from
// 1->0 connections. That is, this is whether the backend is
// being run in "client mode" that requires an active GUI
// connection (such as on Windows by default). Even if this
// is true, the ForceDaemon pref can override this.
resetOnZero bool
bsMu sync.Mutex // lock order: bsMu, then mu bsMu sync.Mutex // lock order: bsMu, then mu
bs *ipn.BackendServer bs *ipn.BackendServer
mu sync.Mutex mu sync.Mutex
allClients map[net.Conn]connIdentity // HTTP or IPN serverModeUser *user.User // or nil if not in server mode
clients map[net.Conn]bool // subset of allClients; only IPN protocol lastUserID string // tracks last userid; on change, Reset state for paranoia
allClients map[net.Conn]connIdentity // HTTP or IPN
clients map[net.Conn]bool // subset of allClients; only IPN protocol
} }
// connIdentity represents the owner of a localhost TCP connection. // connIdentity represents the owner of a localhost TCP connection.
@ -168,6 +183,9 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
return return
} }
// Tell the LocalBackend about the identity we're now running as.
s.b.SetCurrentUserID(ci.UserID)
if isHTTPReq { if isHTTPReq {
httpServer := http.Server{ httpServer := http.Server{
// Localhost connections are cheap; so only do // Localhost connections are cheap; so only do
@ -214,6 +232,18 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
return return
} }
// If the connected user changes, reset the backend server state to make
// sure node keys don't leak between users.
var doReset bool
defer func() {
if doReset {
s.logf("identity changed; resetting server")
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
}()
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -236,12 +266,22 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
return ci, fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid) return ci, fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid)
} }
} }
if su := s.serverModeUser; su != nil && ci.UserID != su.Uid {
//lint:ignore ST1005 we want to capitalize Tailscale here
return ci, fmt.Errorf("Tailscale running in server mode as %s. Access denied.", su.Username)
}
if !isHTTP { if !isHTTP {
s.clients[c] = true s.clients[c] = true
} }
s.allClients[c] = ci s.allClients[c] = ci
if s.lastUserID != ci.UserID {
if s.lastUserID != "" {
doReset = true
}
s.lastUserID = ci.UserID
}
return ci, nil return ci, nil
} }
@ -253,9 +293,14 @@ func (s *server) removeAndCloseConn(c net.Conn) {
s.mu.Unlock() s.mu.Unlock()
if remain == 0 && s.resetOnZero { if remain == 0 && s.resetOnZero {
s.bsMu.Lock() if s.b.RunningAndDaemonForced() {
s.bs.Reset() s.logf("client disconnected; staying alive in server mode")
s.bsMu.Unlock() } else {
s.logf("client disconnected; stopping server")
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
} }
c.Close() c.Close()
} }
@ -270,9 +315,48 @@ func (s *server) stopAll() {
s.clients = nil s.clients = nil
} }
// setServerModeUserLocked is called when we're in server mode but our s.serverModeUser is nil.
//
// s.mu must be held
func (s *server) setServerModeUserLocked() {
var ci connIdentity
var ok bool
for _, ci = range s.allClients {
ok = true
break
}
if !ok {
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
return
}
if ci.Unknown {
return
}
if ci.User != nil {
s.logf("ipnserver: now in server mode; user=%v", ci.User.Username)
s.serverModeUser = ci.User
} else {
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
}
}
func (s *server) writeToClients(b []byte) { func (s *server) writeToClients(b []byte) {
inServerMode := s.b.RunningAndDaemonForced()
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if inServerMode {
if s.serverModeUser == nil {
s.setServerModeUserLocked()
}
} else {
if s.serverModeUser != nil {
s.logf("ipnserver: no longer in server mode")
s.serverModeUser = nil
}
}
for c := range s.clients { for c := range s.clients {
ipn.WriteMsg(c, b) ipn.WriteMsg(c, b)
} }
@ -290,6 +374,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
} }
server := &server{ server := &server{
logf: logf,
resetOnZero: !opts.SurviveDisconnects, resetOnZero: !opts.SurviveDisconnects,
} }
@ -306,12 +391,11 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
logf("Listening on %v", listen.Addr()) logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second) bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine() eng, err := getEngine()
if err != nil { if err != nil {
logf("Initial getEngine call: %v", err) logf("ipnserver: initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ { for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept() c, err := listen.Accept()
if err != nil { if err != nil {
@ -319,14 +403,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
bo.BackOff(ctx, err) bo.BackOff(ctx, err)
continue continue
} }
logf("%d: trying getEngine again...", i) logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine() eng, err = getEngine()
if err == nil { if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i) logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c unservedConn = c
break break
} }
logf("%d: getEngine failed again: %v", i, err) logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error() errMsg := err.Error()
go func() { go func() {
defer c.Close() defer c.Close()
@ -347,6 +431,24 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
if err != nil { if err != nil {
return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err) return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err)
} }
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err)
}
key := string(autoStartKey)
if strings.HasPrefix(key, "user-") {
uid := strings.TrimPrefix(key, "user-")
u, err := user.LookupId(uid)
if err != nil {
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
} else {
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
server.serverModeUser = u
}
opts.AutostartStateKey = ipn.StateKey(key)
}
}
} else { } else {
store = &ipn.MemoryStore{} store = &ipn.MemoryStore{}
} }

@ -66,7 +66,8 @@ type LocalBackend struct {
mu sync.Mutex mu sync.Mutex
notify func(Notify) notify func(Notify)
c *controlclient.Client c *controlclient.Client
stateKey StateKey stateKey StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *Prefs prefs *Prefs
machinePrivKey wgcfg.PrivateKey machinePrivKey wgcfg.PrivateKey
state State state State
@ -786,6 +787,15 @@ func (b *LocalBackend) State() State {
return b.state return b.state
} }
// RunningAndDaemonForced reports whether the backend is currently
// running and the preferences say that Tailscale should run in
// "server mode" (ForceDaemon).
func (b *LocalBackend) RunningAndDaemonForced() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.state == Running && b.prefs != nil && b.prefs.ForceDaemon
}
// getEngineStatus returns a copy of b.engineStatus. // getEngineStatus returns a copy of b.engineStatus.
// //
// TODO(bradfitz): remove this and use Status() throughout. // TODO(bradfitz): remove this and use Status() throughout.
@ -899,6 +909,12 @@ func (b *LocalBackend) shieldsAreUp() bool {
return b.prefs.ShieldsUp return b.prefs.ShieldsUp
} }
func (b *LocalBackend) SetCurrentUserID(uid string) {
b.mu.Lock()
b.userID = uid
b.mu.Unlock()
}
func (b *LocalBackend) SetWantRunning(wantRunning bool) { func (b *LocalBackend) SetWantRunning(wantRunning bool) {
b.mu.Lock() b.mu.Lock()
new := b.prefs.Clone() new := b.prefs.Clone()
@ -935,6 +951,7 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
applyPrefsToHostinfo(newHi, new) applyPrefsToHostinfo(newHi, new)
b.hostinfo = newHi b.hostinfo = newHi
hostInfoChanged := !oldHi.Equal(newHi) hostInfoChanged := !oldHi.Equal(newHi)
userID := b.userID
b.mu.Unlock() b.mu.Unlock()
@ -943,6 +960,26 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
b.logf("Failed to save new controlclient state: %v", err) b.logf("Failed to save new controlclient state: %v", err)
} }
} }
if userID != "" { // e.g. on Windows
if new.ForceDaemon {
stateKey := StateKey("user-" + userID)
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
b.logf("WriteState error: %v", err)
}
// It's important we do this here too, even if it looks
// redundant with the one in the 'if stateKey != ""'
// check block above. That one won't fire in the case
// where the Windows client started up in client mode.
// This happens when we transition into server mode:
if err := b.store.WriteState(stateKey, new.ToBytes()); err != nil {
b.logf("WriteState error: %v", err)
}
} else {
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
b.logf("WriteState error: %v", err)
}
}
}
// [GRINDER STATS LINE] - please don't remove (used for log parsing) // [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.logf("SetPrefs: %v", new.Pretty()) b.logf("SetPrefs: %v", new.Pretty())

@ -11,6 +11,8 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings"
"github.com/tailscale/wireguard-go/wgcfg" "github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/atomicfile" "tailscale.com/atomicfile"
@ -22,40 +24,51 @@ import (
type Prefs struct { type Prefs struct {
// ControlURL is the URL of the control server to use. // ControlURL is the URL of the control server to use.
ControlURL string ControlURL string
// RouteAll specifies whether to accept subnet and default routes // RouteAll specifies whether to accept subnet and default routes
// advertised by other nodes on the Tailscale network. // advertised by other nodes on the Tailscale network.
RouteAll bool RouteAll bool
// AllowSingleHosts specifies whether to install routes for each // AllowSingleHosts specifies whether to install routes for each
// node IP on the tailscale network, in addition to a route for // node IP on the tailscale network, in addition to a route for
// the whole network. // the whole network.
// This corresponds to the "tailscale up --host-routes" value,
// which defaults to true.
// //
// TODO(danderson): why do we have this? It dumps a lot of stuff // TODO(danderson): why do we have this? It dumps a lot of stuff
// into the routing table, and a single network route _should_ be // into the routing table, and a single network route _should_ be
// all that we need. But when I turn this off in my tailscaled, // all that we need. But when I turn this off in my tailscaled,
// packets stop flowing. What's up with that? // packets stop flowing. What's up with that?
AllowSingleHosts bool AllowSingleHosts bool
// CorpDNS specifies whether to install the Tailscale network's // CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists. // DNS configuration, if it exists.
CorpDNS bool CorpDNS bool
// WantRunning indicates whether networking should be active on // WantRunning indicates whether networking should be active on
// this node. // this node.
WantRunning bool WantRunning bool
// ShieldsUp indicates whether to block all incoming connections, // ShieldsUp indicates whether to block all incoming connections,
// regardless of the control-provided packet filter. If false, we // regardless of the control-provided packet filter. If false, we
// use the packet filter as provided. If true, we block incoming // use the packet filter as provided. If true, we block incoming
// connections. // connections.
ShieldsUp bool ShieldsUp bool
// AdvertiseTags specifies groups that this node wants to join, for // AdvertiseTags specifies groups that this node wants to join, for
// purposes of ACL enforcement. These can be referenced from the ACL // purposes of ACL enforcement. These can be referenced from the ACL
// security policy. Note that advertising a tag doesn't guarantee that // security policy. Note that advertising a tag doesn't guarantee that
// the control server will allow you to take on the rights for that // the control server will allow you to take on the rights for that
// tag. // tag.
AdvertiseTags []string AdvertiseTags []string
// Hostname is the hostname to use for identifying the node. If // Hostname is the hostname to use for identifying the node. If
// not set, os.Hostname is used. // not set, os.Hostname is used.
Hostname string Hostname string
// OSVersion overrides tailcfg.Hostinfo's OSVersion. // OSVersion overrides tailcfg.Hostinfo's OSVersion.
OSVersion string OSVersion string
// DeviceModel overrides tailcfg.Hostinfo's DeviceModel. // DeviceModel overrides tailcfg.Hostinfo's DeviceModel.
DeviceModel string DeviceModel string
@ -67,12 +80,25 @@ type Prefs struct {
// users narrow it down a bit. // users narrow it down a bit.
NotepadURLs bool NotepadURLs bool
// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.
ForceDaemon bool `json:"ForceDaemon,omitempty"`
// The following block of options only have an effect on Linux. // The following block of options only have an effect on Linux.
// AdvertiseRoutes specifies CIDR prefixes to advertise into the // AdvertiseRoutes specifies CIDR prefixes to advertise into the
// Tailscale network as reachable through the current // Tailscale network as reachable through the current
// node. // node.
AdvertiseRoutes []wgcfg.CIDR AdvertiseRoutes []wgcfg.CIDR
// NoSNAT specifies whether to source NAT traffic going to // NoSNAT specifies whether to source NAT traffic going to
// destinations in AdvertiseRoutes. The default is to apply source // destinations in AdvertiseRoutes. The default is to apply source
// NAT, which makes the traffic appear to come from the router // NAT, which makes the traffic appear to come from the router
@ -84,6 +110,7 @@ type Prefs struct {
// //
// Linux-only. // Linux-only.
NoSNAT bool NoSNAT bool
// NetfilterMode specifies how much to manage netfilter rules for // NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all. // Tailscale, if at all.
NetfilterMode router.NetfilterMode NetfilterMode router.NetfilterMode
@ -99,16 +126,40 @@ type Prefs struct {
// IsEmpty reports whether p is nil or pointing to a Prefs zero value. // IsEmpty reports whether p is nil or pointing to a Prefs zero value.
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) } func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
func (p *Prefs) Pretty() string { func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
var pp string func (p *Prefs) pretty(goos string) string {
var sb strings.Builder
sb.WriteString("Prefs{")
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
if !p.AllowSingleHosts {
sb.WriteString("mesh=false ")
}
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
if p.ForceDaemon {
sb.WriteString("server=true ")
}
if p.NotepadURLs {
sb.WriteString("notepad=true ")
}
if p.ShieldsUp {
sb.WriteString("shields=true ")
}
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
}
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
}
if goos == "linux" {
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
}
if p.Persist != nil { if p.Persist != nil {
pp = p.Persist.Pretty() sb.WriteString(p.Persist.Pretty())
} else { } else {
pp = "Persist=nil" sb.WriteString("Persist=nil")
} }
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v shields=%v routes=%v snat=%v nf=%v %v}", sb.WriteString("}")
p.RouteAll, p.AllowSingleHosts, p.CorpDNS, p.WantRunning, return sb.String()
p.NotepadURLs, p.ShieldsUp, p.AdvertiseRoutes, !p.NoSNAT, p.NetfilterMode, pp)
} }
func (p *Prefs) ToBytes() []byte { func (p *Prefs) ToBytes() []byte {
@ -140,6 +191,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.Hostname == p2.Hostname && p.Hostname == p2.Hostname &&
p.OSVersion == p2.OSVersion && p.OSVersion == p2.OSVersion &&
p.DeviceModel == p2.DeviceModel && p.DeviceModel == p2.DeviceModel &&
p.ForceDaemon == p2.ForceDaemon &&
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) && compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) && compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist) p.Persist.Equals(p2.Persist)

@ -24,7 +24,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) { func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"} prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n", t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles) have, prefsHandles)
@ -278,3 +278,55 @@ func TestPrefsPersist(t *testing.T) {
} }
checkPrefs(t, p) checkPrefs(t, p)
} }
func TestPrefsPretty(t *testing.T) {
tests := []struct {
p Prefs
os string
want string
}{
{
Prefs{},
"linux",
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
},
{
Prefs{},
"windows",
"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
},
{
Prefs{ShieldsUp: true},
"windows",
"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
},
{
Prefs{AllowSingleHosts: true},
"windows",
"Prefs{ra=false dns=false want=false Persist=nil}",
},
{
Prefs{
NotepadURLs: true,
AllowSingleHosts: true,
},
"windows",
"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
},
{
Prefs{
AllowSingleHosts: true,
WantRunning: true,
ForceDaemon: true, // server mode
},
"windows",
"Prefs{ra=false dns=false want=true server=true Persist=nil}",
},
}
for i, tt := range tests {
got := tt.p.pretty(tt.os)
if got != tt.want {
t.Errorf("%d. wrong String:\n got: %s\nwant: %s\n", i, got, tt.want)
}
}
}

@ -5,6 +5,7 @@
package ipn package ipn
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -33,6 +34,15 @@ const (
// node-global state. To keep open the option of having per-user state // node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username. // later, the global state key doesn't look like a username.
GlobalDaemonStateKey = StateKey("_daemon") 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. // StateStore persists state, and produces it back on request.
@ -132,6 +142,9 @@ func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
func (s *FileStore) WriteState(id StateKey, bs []byte) error { func (s *FileStore) WriteState(id StateKey, bs []byte) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if bytes.Equal(s.cache[id], bs) {
return nil
}
s.cache[id] = append([]byte(nil), bs...) s.cache[id] = append([]byte(nil), bs...)
bs, err := json.MarshalIndent(s.cache, "", " ") bs, err := json.MarshalIndent(s.cache, "", " ")
if err != nil { if err != nil {

Loading…
Cancel
Save