From 2708544018e4b8cdbc35ae8ae18398e3ccf69cbb Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 17 Mar 2022 09:03:02 -0700 Subject: [PATCH] tsnet: add some usability polish, remove WIP env var restriction Change-Id: Id9ec1713c65cdd597d20b03e21e11cd60b54bb6a Signed-off-by: Brad Fitzpatrick --- tsnet/tsnet.go | 149 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 8bee0fd69..390540428 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -9,7 +9,6 @@ package tsnet import ( "context" - "errors" "fmt" "log" "net" @@ -66,12 +65,15 @@ type Server struct { // as an Ephemeral node (https://tailscale.com/kb/1111/ephemeral-nodes/). Ephemeral bool - initOnce sync.Once - initErr error - lb *ipnlocal.LocalBackend - // the state directory - rootPath string - hostname string + initOnce sync.Once + initErr error + lb *ipnlocal.LocalBackend + linkMon *monitor.Mon + localAPIListener net.Listener + rootPath string // the state directory + hostname string + shutdownCtx context.Context + shutdownCancel context.CancelFunc mu sync.Mutex listeners map[listenKey]*listener @@ -94,17 +96,34 @@ func (s *Server) Start() error { return s.initErr } +// Close stops the server. +// +// It must not be called before or concurrently with Start. +func (s *Server) Close() error { + s.shutdownCancel() + s.lb.Shutdown() + s.linkMon.Close() + s.localAPIListener.Close() + + s.mu.Lock() + defer s.mu.Unlock() + for _, ln := range s.listeners { + ln.Close() + } + s.listeners = nil + + return nil +} + func (s *Server) doInit() { + + s.shutdownCtx, s.shutdownCancel = context.WithCancel(context.Background()) if err := s.start(); err != nil { s.initErr = fmt.Errorf("tsnet: %w", err) } } func (s *Server) start() error { - if !envknob.UseWIPCode() { - return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true") - } - exe, err := os.Executable() if err != nil { return err @@ -122,12 +141,18 @@ func (s *Server) start() error { return fmt.Errorf("in-memory store is only supported for Ephemeral nodes") } } + + logf := s.logf + if s.rootPath == "" { confDir, err := os.UserConfigDir() if err != nil { return err } - s.rootPath = filepath.Join(confDir, "tslib-"+prog) + s.rootPath, err = getTSNetDir(logf, confDir, prog) + if err != nil { + return err + } if err := os.MkdirAll(s.rootPath, 0700); err != nil { return err } @@ -138,15 +163,10 @@ func (s *Server) start() error { return fmt.Errorf("%v is not a directory", s.rootPath) } - logf := s.Logf - if logf == nil { - logf = log.Printf - } - // TODO(bradfitz): start logtail? don't use filch, perhaps? // only upload plumbed Logf? - linkMon, err := monitor.New(logf) + s.linkMon, err = monitor.New(logf) if err != nil { return err } @@ -154,7 +174,7 @@ func (s *Server) start() error { s.dialer = new(tsdial.Dialer) // mutated below (before used) eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{ ListenPort: 0, - LinkMonitor: linkMon, + LinkMonitor: s.linkMon, Dialer: s.dialer, }) if err != nil { @@ -184,12 +204,14 @@ func (s *Server) start() error { } if s.Store == nil { - s.Store, err = store.New(logf, filepath.Join(s.rootPath, "tailscaled.state")) + stateFile := filepath.Join(s.rootPath, "tailscaled.state") + logf("tsnet running state path %s", stateFile) + s.Store, err = store.New(logf, stateFile) if err != nil { return err } } - logid := "tslib-TODO" + logid := "tsnet-TODO" // https://github.com/tailscale/tailscale/issues/3866 loginFlags := controlclient.LoginDefault if s.Ephemeral { @@ -200,6 +222,7 @@ func (s *Server) start() error { return fmt.Errorf("NewLocalBackend: %v", err) } lb.SetVarRoot(s.rootPath) + logf("tsnet starting with hostname %q, varRoot %q", s.hostname, s.rootPath) s.lb = lb lb.SetDecompressor(func() (controlclient.Decompressor, error) { return smallzstd.NewDecoder(nil) @@ -207,17 +230,22 @@ func (s *Server) start() error { prefs := ipn.NewPrefs() prefs.Hostname = s.hostname prefs.WantRunning = true + authKey := os.Getenv("TS_AUTHKEY") err = lb.Start(ipn.Options{ StateKey: ipn.GlobalDaemonStateKey, UpdatePrefs: prefs, - AuthKey: os.Getenv("TS_AUTHKEY"), + AuthKey: authKey, }) if err != nil { return fmt.Errorf("starting backend: %w", err) } - if os.Getenv("TS_LOGIN") == "1" || os.Getenv("TS_AUTHKEY") != "" { + if lb.State() == ipn.NeedsLogin || envknob.Bool("TSNET_FORCE_LOGIN") { + logf("LocalBackend state is %v; running StartLoginInteractive...") s.lb.StartLoginInteractive() + } else if authKey != "" { + logf("TS_AUTHKEY is set; but state is %v. Ignoring authkey. Re-run with TSNET_FORCE_LOGIN=1 to force use of authkey.") } + go s.printAuthURLLoop() // Run the localapi handler, to allow fetching LetsEncrypt certs. lah := localapi.NewHandler(lb, logf, logid) @@ -228,6 +256,7 @@ func (s *Server) start() error { // nettest.Listen provides a in-memory pipe based implementation for net.Conn. // TODO(maisem): Rename nettest package to remove "test". lal := nettest.Listen("local-tailscaled.sock:80") + s.localAPIListener = lal // Override the Tailscale client to use the in-process listener. tailscale.TailscaledDialer = lal.Dial @@ -239,6 +268,37 @@ func (s *Server) start() error { return nil } +func (s *Server) logf(format string, a ...interface{}) { + if s.Logf != nil { + s.Logf(format, a...) + return + } + log.Printf(format, a...) +} + +// printAuthURLLoop loops once every few seconds while the server is still running and +// is in NeedsLogin state, printing out the auth URL. +func (s *Server) printAuthURLLoop() { + for { + if s.shutdownCtx.Err() != nil { + return + } + if st := s.lb.State(); st != ipn.NeedsLogin { + s.logf("printAuthURLLoop: state is %v; stopping", st) + return + } + st := s.lb.StatusWithoutPeers() + if st.AuthURL != "" { + s.logf("To start this tsnet server, restart with TS_AUTHKEY set, or go to: %s", st.AuthURL) + } + select { + case <-time.After(5 * time.Second): + case <-s.shutdownCtx.Done(): + return + } + } +} + func (s *Server) forwardTCP(c net.Conn, port uint16) { s.mu.Lock() ln, ok := s.listeners[listenKey{"tcp", "", fmt.Sprint(port)}] @@ -256,6 +316,49 @@ func (s *Server) forwardTCP(c net.Conn, port uint16) { } } +// getTSNetDir usually just returns filepath.Join(confDir, "tsnet-"+prog) +// with no error. +// +// One special case is that it renames old "tslib-" directories to +// "tsnet-", and that rename might return an error. +// +// TODO(bradfitz): remove this maybe 6 months after 2022-03-17, +// once people (notably Tailscale corp services) have updated. +func getTSNetDir(logf logger.Logf, confDir, prog string) (string, error) { + oldPath := filepath.Join(confDir, "tslib-"+prog) + newPath := filepath.Join(confDir, "tsnet-"+prog) + + fi, err := os.Lstat(oldPath) + if os.IsNotExist(err) { + // Common path. + return newPath, nil + } + if err != nil { + return "", err + } + if !fi.IsDir() { + return "", fmt.Errorf("expected old tslib path %q to be a directory; got %v", oldPath, fi.Mode()) + } + + // At this point, oldPath exists and is a directory. But does + // the new path exist? + + fi, err = os.Lstat(newPath) + if err == nil && fi.IsDir() { + // New path already exists somehow. Ignore the old one and + // don't try to migrate it. + return newPath, nil + } + if err != nil && !os.IsNotExist(err) { + return "", err + } + if err := os.Rename(oldPath, newPath); err != nil { + return "", err + } + logf("renamed old tsnet state storage directory %q to %q", oldPath, newPath) + return newPath, nil +} + // Listen announces only on the Tailscale network. // It will start the server if it has not been started yet. func (s *Server) Listen(network, addr string) (net.Listener, error) {