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/portlist from tailscale.com/ipn/ipnlocal
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
tailscale.com/syncs from tailscale.com/net/netcheck+
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/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
@ -45,6 +47,7 @@
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tsweb"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
@ -273,7 +276,21 @@ func statePathOrDefault() string {
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()
o.VarRoot = args.statedir
@ -297,9 +314,6 @@ func ipnServerOpts() (o ipnserver.Options) {
// TODO(bradfitz): if we start using browser LocalStorage
// or something, then rethink this.
o.LoginFlags = controlclient.LoginEphemeral
fallthrough
default:
o.SurviveDisconnects = true
case "windows":
// Not those.
}
@ -445,11 +459,24 @@ func run() error {
if err != nil {
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 {
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 {
log.Fatalf("failed to start netstack: %v", err)
}

View File

@ -31,6 +31,7 @@
"os"
"os/exec"
"os/signal"
"path/filepath"
"sync"
"syscall"
"time"
@ -40,16 +41,20 @@
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/winutil"
"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
// 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)
runDone := make(chan struct{})
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 {
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 {
ns.SetLocalBackend(server.LocalBackend())
ns.SetLocalBackend(lb)
}
return server.Run(ctx, ln)
}

View File

@ -36,6 +36,7 @@
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
@ -124,15 +125,17 @@ func newIPN(jsConfig js.Value) map[string]any {
return ns.DialContextTCP(ctx, dst)
}
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, ipnserver.Options{
SurviveDisconnects: true,
LoginFlags: controlclient.LoginEphemeral,
AutostartStateKey: "wasm",
})
logid := lpc.PublicID.String()
srv := ipnserver.New(logf, logid)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, "wasm", dialer, eng, controlclient.LoginEphemeral)
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)
jsIPN := &jsIPN{

View File

@ -11,69 +11,27 @@
"io"
"net"
"net/http"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"unicode"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/tsdial"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
"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
// TCP or unix socket connections talking to that backend.
type Server struct {
b *ipnlocal.LocalBackend
lb atomic.Pointer[ipnlocal.LocalBackend]
logf logger.Logf
backendLogID string
// 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.
resetOnZero bool
startBackendOnce sync.Once
runCalled atomic.Bool
// mu guards the fields that follow.
// lock order: mu, then LocalBackend.mu
mu sync.Mutex
@ -90,8 +51,13 @@ type Server struct {
activeReqs map[*http.Request]*ipnauth.ConnIdentity
}
// LocalBackend returns the server's LocalBackend.
func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b }
func (s *Server) mustBackend() *ipnlocal.LocalBackend {
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) {
if r.Method == "CONNECT" {
@ -104,6 +70,15 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
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
switch v := r.Context().Value(connIdentityContextKey{}).(type) {
case *ipnauth.ConnIdentity:
@ -124,7 +99,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
defer onDone()
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.PermitCert = s.connCanFetchCerts(ci)
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())}
}
}
if err := s.b.CheckIPNConnectionAllowed(ci); err != nil {
if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil {
return inUseOtherUserError{err}
}
return nil
@ -194,7 +169,7 @@ func (s *Server) localAPIPermissions(ci *ipnauth.ConnIdentity) (read, write bool
return true, true
}
if ci.IsUnixSock() {
return true, !ci.IsReadonlyConn(s.b.OperatorUserID(), logger.Discard)
return true, !ci.IsReadonlyConn(s.mustBackend().OperatorUserID(), logger.Discard)
}
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")
}
lb := s.mustBackend()
// If the connected user changes, reset the backend server state to make
// sure node keys don't leak between users.
var doReset bool
defer func() {
if doReset {
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 {
// 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 != "" {
doReset = true
@ -289,11 +266,11 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
s.mu.Unlock()
if remain == 0 && s.resetOnZero {
if s.b.InServerMode() {
if lb.InServerMode() {
s.logf("client disconnected; staying alive in server mode")
} else {
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.
//
// 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)
if err != nil {
return nil, fmt.Errorf("NewLocalBackend: %v", err)
}
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,
//
// At some point, either before or after Run, the Server's SetLocalBackend
// method must also be called before Server can do anything useful.
func New(logf logger.Logf, logid string) *Server {
return &Server{
backendLogID: logid,
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
@ -351,8 +331,16 @@ type connIdentityContextKey struct{
//
// If the context is done, the listener is closed. It is also the base context
// 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 {
defer s.b.Shutdown()
s.runCalled.Store(true)
defer func() {
if lb := s.lb.Load(); lb != nil {
lb.Shutdown()
}
}()
runDone := make(chan struct{})
defer close(runDone)
@ -367,9 +355,7 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
ln.Close()
}()
if s.b.Prefs().Valid() {
s.b.Start(ipn.Options{})
}
s.startBackendIfNeeded()
systemd.Ready()
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
// is used to run a debug server.
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
// access this.
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-Content-Type-Options", "nosniff")
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?
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/envknob"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store"
_ "tailscale.com/logpolicy"
_ "tailscale.com/logtail"
_ "tailscale.com/net/dns"
_ "tailscale.com/net/dnsfallback"
_ "tailscale.com/net/interfaces"
_ "tailscale.com/net/netns"
_ "tailscale.com/net/portmapper"
@ -32,6 +34,7 @@
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"
_ "tailscale.com/safesocket"
_ "tailscale.com/smallzstd"
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb"

View File

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

View File

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

View File

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

View File

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