mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +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 (
|
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
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user