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 <dave@natulte.net>
This commit is contained in:
David Anderson 2020-02-15 18:14:50 -08:00 committed by Dave Anderson
parent 997678f540
commit 62fb652eef
4 changed files with 85 additions and 59 deletions

View File

@ -8,9 +8,7 @@
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net" "net"
"os" "os"
@ -19,12 +17,20 @@
"github.com/apenwarr/fixconsole" "github.com/apenwarr/fixconsole"
"github.com/pborman/getopt/v2" "github.com/pborman/getopt/v2"
"tailscale.com/atomicfile"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/logpolicy" "tailscale.com/logpolicy"
"tailscale.com/safesocket" "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. // pump receives backend messages on conn and pushes them into bc.
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) { func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
defer log.Printf("Control connection done.\n") defer log.Printf("Control connection done.\n")
@ -45,34 +51,26 @@ func main() {
log.Printf("fixConsoleOutput: %v\n", err) 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") 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") 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") routeall := getopt.BoolLong("remote-routes", 'R', "accept routes advertised by remote nodes")
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node") nopf := getopt.BoolLong("no-packet-filter", 'F', "disable packet filter")
getopt.Parse() getopt.Parse()
if *config == "" { pol := logpolicy.New("tailnode.log.tailscale.io", "tailscale")
logpolicy.New("tailnode.log.tailscale.io", "tailscale")
log.Fatal("no --config provided")
}
if len(getopt.Args()) > 0 { if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", 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() defer pol.Close()
localCfg, err := loadConfig(*config)
if err != nil {
log.Fatal(err)
}
// TODO(apenwarr): fix different semantics between prefs and uflags // TODO(apenwarr): fix different semantics between prefs and uflags
// TODO(apenwarr): allow setting/using CorpDNS // TODO(apenwarr): allow setting/using CorpDNS
prefs := &localCfg prefs := ipn.Prefs{
prefs.WantRunning = true WantRunning: true,
prefs.RouteAll = *rroutes || *droutes RouteAll: *routeall,
prefs.AllowSingleHosts = !*nuroutes AllowSingleHosts: !*nuroutes,
UsePacketFilter: !*nopf,
}
c, err := safesocket.Connect("", "Tailscale", "tailscaled", 41112) c, err := safesocket.Connect("", "Tailscale", "tailscaled", 41112)
if err != nil { if err != nil {
@ -83,6 +81,7 @@ func main() {
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { go func() {
interrupt := make(chan os.Signal, 1) interrupt := make(chan os.Signal, 1)
@ -92,11 +91,11 @@ func main() {
}() }()
bc := ipn.NewBackendClient(log.Printf, clientToServer) bc := ipn.NewBackendClient(log.Printf, clientToServer)
bc.SetPrefs(prefs)
opts := ipn.Options{ opts := ipn.Options{
Prefs: prefs, StateKey: globalStateKey,
ServerURL: *server, ServerURL: *server,
Notify: func(n ipn.Notify) { Notify: func(n ipn.Notify) {
log.Printf("Notify: %v\n", n)
if n.ErrMessage != nil { if n.ErrMessage != nil {
log.Fatalf("backend error: %v\n", *n.ErrMessage) 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) 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: case ipn.Starting, ipn.Running:
// Done full authentication process // Done full authentication process
fmt.Fprintf(os.Stderr, "\ntailscaled is authenticated, nothing more to do.\n\n")
cancel() cancel()
} }
} }
if url := n.BrowseToURL; url != nil { if url := n.BrowseToURL; url != nil {
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url) 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) bc.Start(opts)
pump(ctx, bc, c) 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
}

View File

@ -23,6 +23,15 @@
"tailscale.com/wgengine/magicsock" "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() { func main() {
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap") fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
debug := getopt.StringLong("debug", 0, "", "Address of debug server") 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]) log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
} }
if *statepath == "" {
log.Fatalf("--state is required")
}
if *debug != "" { if *debug != "" {
go runDebugServer(*debug) go runDebugServer(*debug)
} }
@ -60,6 +73,7 @@ func main() {
opts := ipnserver.Options{ opts := ipnserver.Options{
StatePath: *statepath, StatePath: *statepath,
AutostartStateKey: globalStateKey,
SurviveDisconnects: true, SurviveDisconnects: true,
} }
err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e) err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e)

View File

@ -25,11 +25,30 @@
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine" "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 { 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 SurviveDisconnects bool
} }
@ -57,6 +76,12 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
if err != nil { if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err) 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 var store ipn.StateStore
if opts.StatePath != "" { 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) bs := ipn.NewBackendServer(logf, b, serverToClient)
logf("Listening on %v\n", listen.Addr()) if opts.AutostartStateKey != "" {
bs.GotCommand(&ipn.Command{
// Go listeners can't take a context, close it instead. Version: version.LONG,
go func() { Start: &ipn.StartArgs{
<-rctx.Done() Opts: ipn.Options{
listen.Close() ServerURL: defaultLoginServer,
}() StateKey: opts.AutostartStateKey,
},
},
})
}
var oldS net.Conn var oldS net.Conn
//lint:ignore SA4006 ctx is never used, but has to be defined so //lint:ignore SA4006 ctx is never used, but has to be defined so

View File

@ -31,9 +31,12 @@ type FakeExpireAfterArgs struct {
Duration time.Duration 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 { type Command struct {
Version string Version string
// Exactly one of the following must be non-nil.
Quit *NoArgs Quit *NoArgs
Start *StartArgs Start *StartArgs
StartLoginInteractive *NoArgs StartLoginInteractive *NoArgs