|
|
|
|
@ -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) { |
|
|
|
|
|