mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
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:
parent
8049053f86
commit
e8cc78b1af
@ -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
106
cmd/tailscaled/taildrop.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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{
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user