From 62fb652eeffee44e975233a15cdf5d8ab742aa4a Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sat, 15 Feb 2020 18:14:50 -0800 Subject: [PATCH] cmd/tailscaled: run off internal state autonomously. With this change, tailscaled can be restarted and reconnect without interaction from `tailscale`, and `tailscale` is merely there to provide login assistance and adjust preferences. Signed-off-by: David Anderson --- cmd/tailscale/tailscale.go | 78 ++++++++++++++---------------------- cmd/tailscaled/tailscaled.go | 14 +++++++ ipn/ipnserver/server.go | 45 +++++++++++++++++---- ipn/message.go | 7 +++- 4 files changed, 85 insertions(+), 59 deletions(-) diff --git a/cmd/tailscale/tailscale.go b/cmd/tailscale/tailscale.go index 5b597a731..91b9f065a 100644 --- a/cmd/tailscale/tailscale.go +++ b/cmd/tailscale/tailscale.go @@ -8,9 +8,7 @@ import ( "context" - "encoding/json" "fmt" - "io/ioutil" "log" "net" "os" @@ -19,12 +17,20 @@ "github.com/apenwarr/fixconsole" "github.com/pborman/getopt/v2" - "tailscale.com/atomicfile" "tailscale.com/ipn" "tailscale.com/logpolicy" "tailscale.com/safesocket" ) +// globalStateKey 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. +const globalStateKey = "_daemon" + // pump receives backend messages on conn and pushes them into bc. func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) { defer log.Printf("Control connection done.\n") @@ -45,34 +51,26 @@ func main() { log.Printf("fixConsoleOutput: %v\n", err) } - config := getopt.StringLong("config", 'f', "", "path to config file") 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") + routeall := getopt.BoolLong("remote-routes", 'R', "accept routes advertised by remote nodes") + nopf := getopt.BoolLong("no-packet-filter", 'F', "disable packet filter") getopt.Parse() - if *config == "" { - logpolicy.New("tailnode.log.tailscale.io", "tailscale") - log.Fatal("no --config provided") - } + pol := logpolicy.New("tailnode.log.tailscale.io", "tailscale") if len(getopt.Args()) > 0 { log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0]) } - pol := logpolicy.New("tailnode.log.tailscale.io", *config) defer pol.Close() - 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 := &localCfg - prefs.WantRunning = true - prefs.RouteAll = *rroutes || *droutes - prefs.AllowSingleHosts = !*nuroutes + prefs := ipn.Prefs{ + WantRunning: true, + RouteAll: *routeall, + AllowSingleHosts: !*nuroutes, + UsePacketFilter: !*nopf, + } c, err := safesocket.Connect("", "Tailscale", "tailscaled", 41112) if err != nil { @@ -83,6 +81,7 @@ func main() { } ctx, cancel := context.WithCancel(context.Background()) + defer cancel() go func() { interrupt := make(chan os.Signal, 1) @@ -92,11 +91,11 @@ func main() { }() bc := ipn.NewBackendClient(log.Printf, clientToServer) + bc.SetPrefs(prefs) opts := ipn.Options{ - Prefs: prefs, + StateKey: globalStateKey, ServerURL: *server, Notify: func(n ipn.Notify) { - log.Printf("Notify: %v\n", n) if n.ErrMessage != nil { log.Fatalf("backend error: %v\n", *n.ErrMessage) } @@ -108,41 +107,22 @@ func main() { fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", *server) case ipn.Starting, ipn.Running: // Done full authentication process + fmt.Fprintf(os.Stderr, "\ntailscaled is authenticated, nothing more to do.\n\n") cancel() } } if url := n.BrowseToURL; url != nil { fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url) } - if p := n.Prefs; p != nil { - prefs = p - saveConfig(*config, *p) - } }, } + // 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 + // every time the CLI touches anything. + // + // TODO(danderson): redo the frontend/backend API to assume + // ephemeral frontends that read/modify/write state, once + // Windows/Mac state is moved into backend. bc.Start(opts) pump(ctx, bc, c) } - -func loadConfig(path string) (ipn.Prefs, error) { - b, err := ioutil.ReadFile(path) - if os.IsNotExist(err) { - log.Printf("config %s does not exist", path) - return ipn.NewPrefs(), nil - } - return ipn.PrefsFromBytes(b, false) -} - -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) - } - if err := atomicfile.WriteFile(path, b, 0666); err != nil { - return fmt.Errorf("save config: %v", err) - } - return nil -} diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 039016c6f..ef3b95629 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -23,6 +23,15 @@ "tailscale.com/wgengine/magicsock" ) +// globalStateKey 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. +const globalStateKey = "_daemon" + func main() { fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap") debug := getopt.StringLong("debug", 0, "", "Address of debug server") @@ -43,6 +52,10 @@ func main() { log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0]) } + if *statepath == "" { + log.Fatalf("--state is required") + } + if *debug != "" { go runDebugServer(*debug) } @@ -60,6 +73,7 @@ func main() { opts := ipnserver.Options{ StatePath: *statepath, + AutostartStateKey: globalStateKey, SurviveDisconnects: true, } err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index e913ed239..4592fd80d 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -25,11 +25,30 @@ "tailscale.com/logtail/backoff" "tailscale.com/safesocket" "tailscale.com/types/logger" + "tailscale.com/version" "tailscale.com/wgengine" ) +// defaultLoginServer is the login URL used by an auto-starting +// server. +// +// TODO(danderson): the reason this is hardcoded is that the server +// URL is currently not stored in state, but passed in by the +// frontend. This needs to be fixed. +const defaultLoginServer = "https://login.tailscale.com" + +// Options is the configuration of the Tailscale node agent. type Options struct { - StatePath string + // StatePath is the path to the stored agent state. + StatePath string + // AutostartStateKey, if non-empty, immediately starts the agent + // using the given StateKey. If empty, the agent stays idle and + // waits for a frontend to start it. + AutostartStateKey ipn.StateKey + // SurviveDisconnects specifies how the server reacts to its + // frontend disconnecting. If true, the server keeps running on + // its existing state, and accepts new frontend connections. If + // false, the server dumps its state and becomes idle. SurviveDisconnects bool } @@ -57,6 +76,12 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w if err != nil { return fmt.Errorf("safesocket.Listen: %v", err) } + // Go listeners can't take a context, close it instead. + go func() { + <-rctx.Done() + listen.Close() + }() + logf("Listening on %v\n", listen.Addr()) var store ipn.StateStore if opts.StatePath != "" { @@ -86,13 +111,17 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w bs := ipn.NewBackendServer(logf, b, serverToClient) - logf("Listening on %v\n", listen.Addr()) - - // Go listeners can't take a context, close it instead. - go func() { - <-rctx.Done() - listen.Close() - }() + if opts.AutostartStateKey != "" { + bs.GotCommand(&ipn.Command{ + Version: version.LONG, + Start: &ipn.StartArgs{ + Opts: ipn.Options{ + ServerURL: defaultLoginServer, + StateKey: opts.AutostartStateKey, + }, + }, + }) + } var oldS net.Conn //lint:ignore SA4006 ctx is never used, but has to be defined so diff --git a/ipn/message.go b/ipn/message.go index 48f8e0dbb..f326d32a4 100644 --- a/ipn/message.go +++ b/ipn/message.go @@ -31,9 +31,12 @@ type FakeExpireAfterArgs struct { Duration time.Duration } -// A command message sent to the server. Exactly one of these must be non-nil. +// Command is a command message that is JSON encoded and sent by a +// frontend to a backend. type Command struct { - Version string + Version string + + // Exactly one of the following must be non-nil. Quit *NoArgs Start *StartArgs StartLoginInteractive *NoArgs