tsnet: add some usability polish, remove WIP env var restriction

Change-Id: Id9ec1713c65cdd597d20b03e21e11cd60b54bb6a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-03-17 09:03:02 -07:00 committed by Brad Fitzpatrick
parent 997b19545b
commit 2708544018

View File

@ -9,7 +9,6 @@
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) {