ipn/ipnserver: change Server to let LocalBackend be supplied async

This is step 1 of de-special-casing of Windows and letting the
LocalAPI HTTP server start serving immediately, even while the rest of
the world (notably the Engine and its TUN device) are being created,
which can take a few to dozens of seconds on Windows.

With this change, the ipnserver.New function changes to not take an
Engine and to return immediately, not returning an error, and let its
Run run immediately. If its ServeHTTP is called when it doesn't yet
have a LocalBackend, it returns an error. A TODO in there shows where
a future handler will serve status before an engine is available.

Future changes will:

* delete a bunch of tailscaled_windows.go code and use this new API
* add the ipnserver.Server ServerHTTP handler to await the engine
  being available
* use that handler in the Windows GUI client

Updates #6522

Change-Id: Iae94e68c235e850b112a72ea24ad0e0959b568ee
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-11-27 13:07:41 -08:00 committed by Brad Fitzpatrick
parent 8049053f86
commit e8cc78b1af
11 changed files with 270 additions and 182 deletions

View File

@ -250,7 +250,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/paths from tailscale.com/ipn/ipnlocal+ tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal 💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+ tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ tailscale.com/smallzstd from tailscale.com/cmd/tailscaled+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/net/netcheck+ tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+

106
cmd/tailscaled/taildrop.go Normal file
View File

@ -0,0 +1,106 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.19
package main
import (
"fmt"
"os"
"path/filepath"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
)
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
dg := distro.Get()
switch dg {
case distro.Synology, distro.TrueNAS, distro.QNAP:
// See if they have a "Taildrop" share.
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
path, err := findTaildropDir(dg)
if err != nil {
logf("%s Taildrop support: %v", dg, err)
} else {
logf("%s Taildrop: using %v", dg, path)
lb.SetDirectFileRoot(path)
lb.SetDirectFileDoFinalRename(true)
}
}
}
func findTaildropDir(dg distro.Distro) (string, error) {
const name = "Taildrop"
switch dg {
case distro.Synology:
return findSynologyTaildropDir(name)
case distro.TrueNAS:
return findTrueNASTaildropDir(name)
case distro.QNAP:
return findQnapTaildropDir(name)
}
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
}
// findSynologyTaildropDir looks for the first volume containing a
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
// but on DSM7 at least, we lack permissions to run that.
func findSynologyTaildropDir(name string) (dir string, err error) {
for i := 1; i <= 16; i++ {
dir = fmt.Sprintf("/volume%v/%s", i, name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findTrueNASTaildropDir returns the first matching directory of
// /mnt/{name} or /mnt/*/{name}
func findTrueNASTaildropDir(name string) (dir string, err error) {
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
dir = fmt.Sprintf("/mnt/%s", name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := os.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
for _, fi := range fis {
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findQnapTaildropDir checks if a Shared Folder named "Taildrop" exists.
func findQnapTaildropDir(name string) (string, error) {
dir := fmt.Sprintf("/share/%s", name)
fi, err := os.Stat(dir)
if err != nil {
return "", fmt.Errorf("shared folder %q not found", name)
}
if fi.IsDir() {
return dir, nil
}
// share/Taildrop is usually a symlink to CACHEDEV1_DATA/Taildrop/ or some such.
fullpath, err := filepath.EvalSymlinks(dir)
if err != nil {
return "", fmt.Errorf("symlink to shared folder %q not found", name)
}
if fi, err = os.Stat(fullpath); err == nil && fi.IsDir() {
return dir, nil // return the symlink, how QNAP set it up
}
return "", fmt.Errorf("shared folder %q not found", name)
}

View File

@ -33,11 +33,13 @@
"tailscale.com/cmd/tailscaled/childproc" "tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient" "tailscale.com/control/controlclient"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver" "tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store" "tailscale.com/ipn/store"
"tailscale.com/logpolicy" "tailscale.com/logpolicy"
"tailscale.com/logtail" "tailscale.com/logtail"
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netns" "tailscale.com/net/netns"
"tailscale.com/net/proxymux" "tailscale.com/net/proxymux"
"tailscale.com/net/socks5" "tailscale.com/net/socks5"
@ -45,6 +47,7 @@
"tailscale.com/net/tstun" "tailscale.com/net/tstun"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tsweb" "tailscale.com/tsweb"
"tailscale.com/types/flagtype" "tailscale.com/types/flagtype"
"tailscale.com/types/logger" "tailscale.com/types/logger"
@ -273,7 +276,21 @@ func statePathOrDefault() string {
return "" return ""
} }
func ipnServerOpts() (o ipnserver.Options) { // serverOptions is the configuration of the Tailscale node agent.
type serverOptions struct {
// VarRoot is the Tailscale daemon's private writable
// directory (usually "/var/lib/tailscale" on Linux) that
// contains the "tailscaled.state" file, the "certs" directory
// for TLS certs, and the "files" directory for incoming
// Taildrop files before they're moved to a user directory.
// If empty, Taildrop and TLS certs don't function.
VarRoot string
// LoginFlags specifies the LoginFlags to pass to the client.
LoginFlags controlclient.LoginFlags
}
func ipnServerOpts() (o serverOptions) {
goos := envknob.GOOS() goos := envknob.GOOS()
o.VarRoot = args.statedir o.VarRoot = args.statedir
@ -297,9 +314,6 @@ func ipnServerOpts() (o ipnserver.Options) {
// TODO(bradfitz): if we start using browser LocalStorage // TODO(bradfitz): if we start using browser LocalStorage
// or something, then rethink this. // or something, then rethink this.
o.LoginFlags = controlclient.LoginEphemeral o.LoginFlags = controlclient.LoginEphemeral
fallthrough
default:
o.SurviveDisconnects = true
case "windows": case "windows":
// Not those. // Not those.
} }
@ -445,11 +459,24 @@ func run() error {
if err != nil { if err != nil {
return fmt.Errorf("store.New: %w", err) return fmt.Errorf("store.New: %w", err)
} }
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, opts) logid := pol.PublicID.String()
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, "", dialer, e, opts.LoginFlags)
if err != nil { if err != nil {
return fmt.Errorf("ipnserver.New: %w", err) return fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
} }
ns.SetLocalBackend(srv.LocalBackend()) lb.SetVarRoot(opts.VarRoot)
if root := lb.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
}
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
})
configureTaildrop(logf, lb)
srv := ipnserver.New(logf, logid)
srv.SetLocalBackend(lb)
ns.SetLocalBackend(lb)
if err := ns.Start(); err != nil { if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err) log.Fatalf("failed to start netstack: %v", err)
} }

View File

@ -31,6 +31,7 @@
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -40,16 +41,20 @@
"golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/eventlog"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/control/controlclient"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver" "tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store" "tailscale.com/ipn/store"
"tailscale.com/logpolicy" "tailscale.com/logpolicy"
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/net/tstun" "tailscale.com/net/tstun"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/winutil" "tailscale.com/util/winutil"
"tailscale.com/version" "tailscale.com/version"
@ -609,7 +614,7 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
// //
// This works around issues on Windows with the wgengine.Engine/wintun creation // This works around issues on Windows with the wgengine.Engine/wintun creation
// failing or hanging. See https://github.com/tailscale/tailscale/issues/6522. // failing or hanging. See https://github.com/tailscale/tailscale/issues/6522.
func ipnServerRunWithRetries(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, *netstack.Impl, error), opts ipnserver.Options) error { func ipnServerRunWithRetries(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, *netstack.Impl, error), opts serverOptions) error {
getEngine = getEngineUntilItWorksWrapper(getEngine) getEngine = getEngineUntilItWorksWrapper(getEngine)
runDone := make(chan struct{}) runDone := make(chan struct{})
defer close(runDone) defer close(runDone)
@ -662,12 +667,23 @@ func ipnServerRunWithRetries(ctx context.Context, logf logger.Logf, ln net.Liste
} }
} }
server, err := ipnserver.New(logf, logid, store, eng, dialer, opts) server := ipnserver.New(logf, logid)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, "", dialer, eng, opts.LoginFlags)
if err != nil { if err != nil {
return err return fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
} }
lb.SetVarRoot(opts.VarRoot)
if root := lb.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
}
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
})
server.SetLocalBackend(lb)
if ns != nil { if ns != nil {
ns.SetLocalBackend(server.LocalBackend()) ns.SetLocalBackend(lb)
} }
return server.Run(ctx, ln) return server.Run(ctx, ln)
} }

View File

@ -36,6 +36,7 @@
"tailscale.com/net/netns" "tailscale.com/net/netns"
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/wgengine" "tailscale.com/wgengine"
"tailscale.com/wgengine/netstack" "tailscale.com/wgengine/netstack"
@ -124,15 +125,17 @@ func newIPN(jsConfig js.Value) map[string]any {
return ns.DialContextTCP(ctx, dst) return ns.DialContextTCP(ctx, dst)
} }
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, ipnserver.Options{ logid := lpc.PublicID.String()
SurviveDisconnects: true, srv := ipnserver.New(logf, logid)
LoginFlags: controlclient.LoginEphemeral,
AutostartStateKey: "wasm", lb, err := ipnlocal.NewLocalBackend(logf, logid, store, "wasm", dialer, eng, controlclient.LoginEphemeral)
})
if err != nil { if err != nil {
log.Fatalf("ipnserver.New: %v", err) log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
} }
lb := srv.LocalBackend() lb.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
})
srv.SetLocalBackend(lb)
ns.SetLocalBackend(lb) ns.SetLocalBackend(lb)
jsIPN := &jsIPN{ jsIPN := &jsIPN{

View File

@ -11,69 +11,27 @@
"io" "io"
"net" "net"
"net/http" "net/http"
"os"
"os/user" "os/user"
"path/filepath"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"unicode" "unicode"
"tailscale.com/control/controlclient"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi" "tailscale.com/ipn/localapi"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/tsdial"
"tailscale.com/smallzstd"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/mak" "tailscale.com/util/mak"
"tailscale.com/util/systemd" "tailscale.com/util/systemd"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
) )
// Options is the configuration of the Tailscale node agent.
type Options struct {
// VarRoot is the Tailscale daemon's private writable
// directory (usually "/var/lib/tailscale" on Linux) that
// contains the "tailscaled.state" file, the "certs" directory
// for TLS certs, and the "files" directory for incoming
// Taildrop files before they're moved to a user directory.
// If empty, Taildrop and TLS certs don't function.
VarRoot 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.
//
// This is effectively whether the platform is in "server
// mode" by default. On Linux, it's true; on Windows, it's
// false. But on some platforms (currently only Windows), the
// "server mode" can be overridden at runtime with a change in
// Prefs.ForceDaemon/WantRunning.
//
// To support CLI connections (notably, "tailscale status"),
// the actual definition of "disconnect" is when the
// connection count transitions from 1 to 0.
SurviveDisconnects bool
// LoginFlags specifies the LoginFlags to pass to the client.
LoginFlags controlclient.LoginFlags
}
// Server is an IPN backend and its set of 0 or more active localhost // Server is an IPN backend and its set of 0 or more active localhost
// TCP or unix socket connections talking to that backend. // TCP or unix socket connections talking to that backend.
type Server struct { type Server struct {
b *ipnlocal.LocalBackend lb atomic.Pointer[ipnlocal.LocalBackend]
logf logger.Logf logf logger.Logf
backendLogID string backendLogID string
// resetOnZero is whether to call bs.Reset on transition from // resetOnZero is whether to call bs.Reset on transition from
@ -83,6 +41,9 @@ type Server struct {
// is true, the ForceDaemon pref can override this. // is true, the ForceDaemon pref can override this.
resetOnZero bool resetOnZero bool
startBackendOnce sync.Once
runCalled atomic.Bool
// mu guards the fields that follow. // mu guards the fields that follow.
// lock order: mu, then LocalBackend.mu // lock order: mu, then LocalBackend.mu
mu sync.Mutex mu sync.Mutex
@ -90,8 +51,13 @@ type Server struct {
activeReqs map[*http.Request]*ipnauth.ConnIdentity activeReqs map[*http.Request]*ipnauth.ConnIdentity
} }
// LocalBackend returns the server's LocalBackend. func (s *Server) mustBackend() *ipnlocal.LocalBackend {
func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b } lb := s.lb.Load()
if lb == nil {
panic("unexpected: call to mustBackend in path where SetLocalBackend should've been called")
}
return lb
}
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "CONNECT" { if r.Method == "CONNECT" {
@ -104,6 +70,15 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
// TODO(bradfitz): add a status HTTP handler that returns whether there's a
// LocalBackend yet, optionally blocking until there is one. See
// https://github.com/tailscale/tailscale/issues/6522
lb := s.lb.Load()
if lb == nil {
http.Error(w, "no backend", http.StatusServiceUnavailable)
return
}
var ci *ipnauth.ConnIdentity var ci *ipnauth.ConnIdentity
switch v := r.Context().Value(connIdentityContextKey{}).(type) { switch v := r.Context().Value(connIdentityContextKey{}).(type) {
case *ipnauth.ConnIdentity: case *ipnauth.ConnIdentity:
@ -124,7 +99,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
defer onDone() defer onDone()
if strings.HasPrefix(r.URL.Path, "/localapi/") { if strings.HasPrefix(r.URL.Path, "/localapi/") {
lah := localapi.NewHandler(s.b, s.logf, s.backendLogID) lah := localapi.NewHandler(lb, s.logf, s.backendLogID)
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci) lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
lah.PermitCert = s.connCanFetchCerts(ci) lah.PermitCert = s.connCanFetchCerts(ci)
lah.ServeHTTP(w, r) lah.ServeHTTP(w, r)
@ -171,7 +146,7 @@ func (s *Server) checkConnIdentityLocked(ci *ipnauth.ConnIdentity) error {
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User().Username, active.Pid())} return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User().Username, active.Pid())}
} }
} }
if err := s.b.CheckIPNConnectionAllowed(ci); err != nil { if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil {
return inUseOtherUserError{err} return inUseOtherUserError{err}
} }
return nil return nil
@ -194,7 +169,7 @@ func (s *Server) localAPIPermissions(ci *ipnauth.ConnIdentity) (read, write bool
return true, true return true, true
} }
if ci.IsUnixSock() { if ci.IsUnixSock() {
return true, !ci.IsReadonlyConn(s.b.OperatorUserID(), logger.Discard) return true, !ci.IsReadonlyConn(s.mustBackend().OperatorUserID(), logger.Discard)
} }
return false, false return false, false
} }
@ -252,13 +227,15 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
return nil, errors.New("internal error: nil connIdentity") return nil, errors.New("internal error: nil connIdentity")
} }
lb := s.mustBackend()
// If the connected user changes, reset the backend server state to make // If the connected user changes, reset the backend server state to make
// sure node keys don't leak between users. // sure node keys don't leak between users.
var doReset bool var doReset bool
defer func() { defer func() {
if doReset { if doReset {
s.logf("identity changed; resetting server") s.logf("identity changed; resetting server")
s.b.ResetForClientDisconnect() lb.ResetForClientDisconnect()
} }
}() }()
@ -273,7 +250,7 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
if uid := ci.WindowsUserID(); uid != "" && len(s.activeReqs) == 1 { if uid := ci.WindowsUserID(); uid != "" && len(s.activeReqs) == 1 {
// Tell the LocalBackend about the identity we're now running as. // Tell the LocalBackend about the identity we're now running as.
s.b.SetCurrentUserID(uid) lb.SetCurrentUserID(uid)
if s.lastUserID != uid { if s.lastUserID != uid {
if s.lastUserID != "" { if s.lastUserID != "" {
doReset = true doReset = true
@ -289,11 +266,11 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
s.mu.Unlock() s.mu.Unlock()
if remain == 0 && s.resetOnZero { if remain == 0 && s.resetOnZero {
if s.b.InServerMode() { if lb.InServerMode() {
s.logf("client disconnected; staying alive in server mode") s.logf("client disconnected; staying alive in server mode")
} else { } else {
s.logf("client disconnected; stopping server") s.logf("client disconnected; stopping server")
s.b.ResetForClientDisconnect() lb.ResetForClientDisconnect()
} }
} }
} }
@ -304,43 +281,46 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
// New returns a new Server. // New returns a new Server.
// //
// To start it, use the Server.Run method. // To start it, use the Server.Run method.
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, dialer *tsdial.Dialer, opts Options) (*Server, error) { //
b, err := ipnlocal.NewLocalBackend(logf, logid, store, opts.AutostartStateKey, dialer, eng, opts.LoginFlags) // At some point, either before or after Run, the Server's SetLocalBackend
if err != nil { // method must also be called before Server can do anything useful.
return nil, fmt.Errorf("NewLocalBackend: %v", err) func New(logf logger.Logf, logid string) *Server {
} return &Server{
b.SetVarRoot(opts.VarRoot)
b.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
})
if root := b.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
}
dg := distro.Get()
switch dg {
case distro.Synology, distro.TrueNAS, distro.QNAP:
// See if they have a "Taildrop" share.
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
path, err := findTaildropDir(dg)
if err != nil {
logf("%s Taildrop support: %v", dg, err)
} else {
logf("%s Taildrop: using %v", dg, path)
b.SetDirectFileRoot(path)
b.SetDirectFileDoFinalRename(true)
}
}
server := &Server{
b: b,
backendLogID: logid, backendLogID: logid,
logf: logf, logf: logf,
resetOnZero: !opts.SurviveDisconnects, resetOnZero: envknob.GOOS() == "windows",
}
}
// SetLocalBackend sets the server's LocalBackend.
//
// If b.Run has already been called, then lb.Start will be called.
// Otherwise Start will be called once Run is called.
func (s *Server) SetLocalBackend(lb *ipnlocal.LocalBackend) {
if lb == nil {
panic("nil LocalBackend")
}
if !s.lb.CompareAndSwap(nil, lb) {
panic("already set")
}
s.startBackendIfNeeded()
// TODO(bradfitz): send status update to GUI long poller waiter. See
// https://github.com/tailscale/tailscale/issues/6522
}
func (b *Server) startBackendIfNeeded() {
if !b.runCalled.Load() {
return
}
lb := b.lb.Load()
if lb == nil {
return
}
if lb.Prefs().Valid() {
b.startBackendOnce.Do(func() {
lb.Start(ipn.Options{})
})
} }
return server, nil
} }
// connIdentityContextKey is the http.Request.Context's context.Value key for either an // connIdentityContextKey is the http.Request.Context's context.Value key for either an
@ -351,8 +331,16 @@ type connIdentityContextKey struct{
// //
// If the context is done, the listener is closed. It is also the base context // If the context is done, the listener is closed. It is also the base context
// of all HTTP requests. // of all HTTP requests.
//
// If the Server's LocalBackend has already been set, Run starts it.
// Otherwise, the next call to SetLocalBackend will start it.
func (s *Server) Run(ctx context.Context, ln net.Listener) error { func (s *Server) Run(ctx context.Context, ln net.Listener) error {
defer s.b.Shutdown() s.runCalled.Store(true)
defer func() {
if lb := s.lb.Load(); lb != nil {
lb.Shutdown()
}
}()
runDone := make(chan struct{}) runDone := make(chan struct{})
defer close(runDone) defer close(runDone)
@ -367,9 +355,7 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
ln.Close() ln.Close()
}() }()
if s.b.Prefs().Valid() { s.startBackendIfNeeded()
s.b.Start(ipn.Options{})
}
systemd.Ready() systemd.Ready()
hs := &http.Server{ hs := &http.Server{
@ -404,6 +390,12 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
// Windows and via $DEBUG_LISTENER/debug/ipn when tailscaled's --debug flag // Windows and via $DEBUG_LISTENER/debug/ipn when tailscaled's --debug flag
// is used to run a debug server. // is used to run a debug server.
func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
lb := s.lb.Load()
if lb == nil {
http.Error(w, "no LocalBackend", http.StatusServiceUnavailable)
return
}
// As this is only meant for debug, verify there's no DNS name being used to // As this is only meant for debug, verify there's no DNS name being used to
// access this. // access this.
if !strings.HasPrefix(r.Host, "localhost:") && strings.IndexFunc(r.Host, unicode.IsLetter) != -1 { if !strings.HasPrefix(r.Host, "localhost:") && strings.IndexFunc(r.Host, unicode.IsLetter) != -1 {
@ -415,78 +407,7 @@ func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := s.b.Status() st := lb.Status()
// TODO(bradfitz): add LogID and opts to st? // TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w) st.WriteHTML(w)
} }
func findTaildropDir(dg distro.Distro) (string, error) {
const name = "Taildrop"
switch dg {
case distro.Synology:
return findSynologyTaildropDir(name)
case distro.TrueNAS:
return findTrueNASTaildropDir(name)
case distro.QNAP:
return findQnapTaildropDir(name)
}
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
}
// findSynologyTaildropDir looks for the first volume containing a
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
// but on DSM7 at least, we lack permissions to run that.
func findSynologyTaildropDir(name string) (dir string, err error) {
for i := 1; i <= 16; i++ {
dir = fmt.Sprintf("/volume%v/%s", i, name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findTrueNASTaildropDir returns the first matching directory of
// /mnt/{name} or /mnt/*/{name}
func findTrueNASTaildropDir(name string) (dir string, err error) {
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
dir = fmt.Sprintf("/mnt/%s", name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := os.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
for _, fi := range fis {
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
return dir, nil
}
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findQnapTaildropDir checks if a Shared Folder named "Taildrop" exists.
func findQnapTaildropDir(name string) (string, error) {
dir := fmt.Sprintf("/share/%s", name)
fi, err := os.Stat(dir)
if err != nil {
return "", fmt.Errorf("shared folder %q not found", name)
}
if fi.IsDir() {
return dir, nil
}
// share/Taildrop is usually a symlink to CACHEDEV1_DATA/Taildrop/ or some such.
fullpath, err := filepath.EvalSymlinks(dir)
if err != nil {
return "", fmt.Errorf("symlink to shared folder %q not found", name)
}
if fi, err = os.Stat(fullpath); err == nil && fi.IsDir() {
return dir, nil // return the symlink, how QNAP set it up
}
return "", fmt.Errorf("shared folder %q not found", name)
}

View File

@ -17,11 +17,13 @@
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver" _ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store" _ "tailscale.com/ipn/store"
_ "tailscale.com/logpolicy" _ "tailscale.com/logpolicy"
_ "tailscale.com/logtail" _ "tailscale.com/logtail"
_ "tailscale.com/net/dns" _ "tailscale.com/net/dns"
_ "tailscale.com/net/dnsfallback"
_ "tailscale.com/net/interfaces" _ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns" _ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper" _ "tailscale.com/net/portmapper"
@ -32,6 +34,7 @@
_ "tailscale.com/net/tstun" _ "tailscale.com/net/tstun"
_ "tailscale.com/paths" _ "tailscale.com/paths"
_ "tailscale.com/safesocket" _ "tailscale.com/safesocket"
_ "tailscale.com/smallzstd"
_ "tailscale.com/ssh/tailssh" _ "tailscale.com/ssh/tailssh"
_ "tailscale.com/tailcfg" _ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb" _ "tailscale.com/tsweb"

View File

@ -17,11 +17,13 @@
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver" _ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store" _ "tailscale.com/ipn/store"
_ "tailscale.com/logpolicy" _ "tailscale.com/logpolicy"
_ "tailscale.com/logtail" _ "tailscale.com/logtail"
_ "tailscale.com/net/dns" _ "tailscale.com/net/dns"
_ "tailscale.com/net/dnsfallback"
_ "tailscale.com/net/interfaces" _ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns" _ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper" _ "tailscale.com/net/portmapper"
@ -32,6 +34,7 @@
_ "tailscale.com/net/tstun" _ "tailscale.com/net/tstun"
_ "tailscale.com/paths" _ "tailscale.com/paths"
_ "tailscale.com/safesocket" _ "tailscale.com/safesocket"
_ "tailscale.com/smallzstd"
_ "tailscale.com/ssh/tailssh" _ "tailscale.com/ssh/tailssh"
_ "tailscale.com/tailcfg" _ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb" _ "tailscale.com/tsweb"

View File

@ -17,11 +17,13 @@
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver" _ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store" _ "tailscale.com/ipn/store"
_ "tailscale.com/logpolicy" _ "tailscale.com/logpolicy"
_ "tailscale.com/logtail" _ "tailscale.com/logtail"
_ "tailscale.com/net/dns" _ "tailscale.com/net/dns"
_ "tailscale.com/net/dnsfallback"
_ "tailscale.com/net/interfaces" _ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns" _ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper" _ "tailscale.com/net/portmapper"
@ -32,6 +34,7 @@
_ "tailscale.com/net/tstun" _ "tailscale.com/net/tstun"
_ "tailscale.com/paths" _ "tailscale.com/paths"
_ "tailscale.com/safesocket" _ "tailscale.com/safesocket"
_ "tailscale.com/smallzstd"
_ "tailscale.com/ssh/tailssh" _ "tailscale.com/ssh/tailssh"
_ "tailscale.com/tailcfg" _ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb" _ "tailscale.com/tsweb"

View File

@ -17,11 +17,13 @@
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver" _ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store" _ "tailscale.com/ipn/store"
_ "tailscale.com/logpolicy" _ "tailscale.com/logpolicy"
_ "tailscale.com/logtail" _ "tailscale.com/logtail"
_ "tailscale.com/net/dns" _ "tailscale.com/net/dns"
_ "tailscale.com/net/dnsfallback"
_ "tailscale.com/net/interfaces" _ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns" _ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper" _ "tailscale.com/net/portmapper"
@ -32,6 +34,7 @@
_ "tailscale.com/net/tstun" _ "tailscale.com/net/tstun"
_ "tailscale.com/paths" _ "tailscale.com/paths"
_ "tailscale.com/safesocket" _ "tailscale.com/safesocket"
_ "tailscale.com/smallzstd"
_ "tailscale.com/tailcfg" _ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb" _ "tailscale.com/tsweb"
_ "tailscale.com/types/flagtype" _ "tailscale.com/types/flagtype"

View File

@ -22,12 +22,14 @@
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver" _ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store" _ "tailscale.com/ipn/store"
_ "tailscale.com/logpolicy" _ "tailscale.com/logpolicy"
_ "tailscale.com/logtail" _ "tailscale.com/logtail"
_ "tailscale.com/logtail/backoff" _ "tailscale.com/logtail/backoff"
_ "tailscale.com/net/dns" _ "tailscale.com/net/dns"
_ "tailscale.com/net/dnsfallback"
_ "tailscale.com/net/interfaces" _ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns" _ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper" _ "tailscale.com/net/portmapper"
@ -38,6 +40,7 @@
_ "tailscale.com/net/tstun" _ "tailscale.com/net/tstun"
_ "tailscale.com/paths" _ "tailscale.com/paths"
_ "tailscale.com/safesocket" _ "tailscale.com/safesocket"
_ "tailscale.com/smallzstd"
_ "tailscale.com/tailcfg" _ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb" _ "tailscale.com/tsweb"
_ "tailscale.com/types/flagtype" _ "tailscale.com/types/flagtype"