mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
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:
parent
997678f540
commit
62fb652eef
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user