mirror of
https://github.com/tailscale/tailscale.git
synced 2025-09-21 13:41:46 +02:00
feature/drive: start factoring out Taildrive, add ts_omit_drive build tag
As of this commit (per the issue), the Taildrive code remains where it was, but in new files that are protected by the new ts_omit_drive build tag. Future commits will move it. Updates #17058 Change-Id: Idf0a51db59e41ae8da6ea2b11d238aefc48b219e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
82c5024f03
commit
a1dcf12b67
@ -41,7 +41,7 @@ while [ "$#" -gt 1 ]; do
|
|||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
ldflags="$ldflags -w -s"
|
ldflags="$ldflags -w -s"
|
||||||
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture,ts_omit_relayserver,ts_omit_systray,ts_omit_taildrop,ts_omit_tpm,ts_omit_syspolicy,ts_omit_debugeventbus,ts_omit_webclient"
|
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture,ts_omit_relayserver,ts_omit_systray,ts_omit_taildrop,ts_omit_tpm,ts_omit_syspolicy,ts_omit_debugeventbus,ts_omit_webclient,ts_omit_drive"
|
||||||
;;
|
;;
|
||||||
--box)
|
--box)
|
||||||
if [ ! -z "${TAGS:-}" ]; then
|
if [ ! -z "${TAGS:-}" ]; then
|
||||||
|
@ -210,6 +210,7 @@ func noDupFlagify(c *ffcli.Command) {
|
|||||||
var fileCmd func() *ffcli.Command
|
var fileCmd func() *ffcli.Command
|
||||||
var sysPolicyCmd func() *ffcli.Command
|
var sysPolicyCmd func() *ffcli.Command
|
||||||
var maybeWebCmd func() *ffcli.Command
|
var maybeWebCmd func() *ffcli.Command
|
||||||
|
var maybeDriveCmd func() *ffcli.Command
|
||||||
|
|
||||||
func newRootCmd() *ffcli.Command {
|
func newRootCmd() *ffcli.Command {
|
||||||
rootfs := newFlagSet("tailscale")
|
rootfs := newFlagSet("tailscale")
|
||||||
@ -262,7 +263,7 @@ change in the future.
|
|||||||
updateCmd,
|
updateCmd,
|
||||||
whoisCmd,
|
whoisCmd,
|
||||||
debugCmd(),
|
debugCmd(),
|
||||||
driveCmd,
|
nilOrCall(maybeDriveCmd),
|
||||||
idTokenCmd,
|
idTokenCmd,
|
||||||
configureHostCmd(),
|
configureHostCmd(),
|
||||||
systrayCmd,
|
systrayCmd,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_drive
|
||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -20,43 +22,49 @@ const (
|
|||||||
driveListUsage = "tailscale drive list"
|
driveListUsage = "tailscale drive list"
|
||||||
)
|
)
|
||||||
|
|
||||||
var driveCmd = &ffcli.Command{
|
func init() {
|
||||||
Name: "drive",
|
maybeDriveCmd = driveCmd
|
||||||
ShortHelp: "Share a directory with your tailnet",
|
}
|
||||||
ShortUsage: strings.Join([]string{
|
|
||||||
driveShareUsage,
|
func driveCmd() *ffcli.Command {
|
||||||
driveRenameUsage,
|
return &ffcli.Command{
|
||||||
driveUnshareUsage,
|
Name: "drive",
|
||||||
driveListUsage,
|
ShortHelp: "Share a directory with your tailnet",
|
||||||
}, "\n"),
|
ShortUsage: strings.Join([]string{
|
||||||
LongHelp: buildShareLongHelp(),
|
driveShareUsage,
|
||||||
UsageFunc: usageFuncNoDefaultValues,
|
driveRenameUsage,
|
||||||
Subcommands: []*ffcli.Command{
|
driveUnshareUsage,
|
||||||
{
|
driveListUsage,
|
||||||
Name: "share",
|
}, "\n"),
|
||||||
ShortUsage: driveShareUsage,
|
LongHelp: buildShareLongHelp(),
|
||||||
Exec: runDriveShare,
|
UsageFunc: usageFuncNoDefaultValues,
|
||||||
ShortHelp: "[ALPHA] Create or modify a share",
|
Subcommands: []*ffcli.Command{
|
||||||
|
{
|
||||||
|
Name: "share",
|
||||||
|
ShortUsage: driveShareUsage,
|
||||||
|
Exec: runDriveShare,
|
||||||
|
ShortHelp: "[ALPHA] Create or modify a share",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "rename",
|
||||||
|
ShortUsage: driveRenameUsage,
|
||||||
|
ShortHelp: "[ALPHA] Rename a share",
|
||||||
|
Exec: runDriveRename,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "unshare",
|
||||||
|
ShortUsage: driveUnshareUsage,
|
||||||
|
ShortHelp: "[ALPHA] Remove a share",
|
||||||
|
Exec: runDriveUnshare,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
ShortUsage: driveListUsage,
|
||||||
|
ShortHelp: "[ALPHA] List current shares",
|
||||||
|
Exec: runDriveList,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
Name: "rename",
|
|
||||||
ShortUsage: driveRenameUsage,
|
|
||||||
ShortHelp: "[ALPHA] Rename a share",
|
|
||||||
Exec: runDriveRename,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "unshare",
|
|
||||||
ShortUsage: driveUnshareUsage,
|
|
||||||
ShortHelp: "[ALPHA] Remove a share",
|
|
||||||
Exec: runDriveUnshare,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "list",
|
|
||||||
ShortUsage: driveListUsage,
|
|
||||||
ShortHelp: "[ALPHA] List current shares",
|
|
||||||
Exec: runDriveList,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDriveShare is the entry point for the "tailscale drive share" command.
|
// runDriveShare is the entry point for the "tailscale drive share" command.
|
||||||
|
@ -274,6 +274,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/feature from tailscale.com/feature/wakeonlan+
|
tailscale.com/feature from tailscale.com/feature/wakeonlan+
|
||||||
tailscale.com/feature/capture from tailscale.com/feature/condregister
|
tailscale.com/feature/capture from tailscale.com/feature/condregister
|
||||||
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
|
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
|
||||||
|
tailscale.com/feature/drive from tailscale.com/feature/condregister
|
||||||
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
|
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
|
||||||
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
|
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
|
||||||
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
|
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
|
||||||
|
@ -61,3 +61,19 @@ func TestOmitReflectThings(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}.Check(t)
|
}.Check(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOmitDrive(t *testing.T) {
|
||||||
|
deptest.DepChecker{
|
||||||
|
GOOS: "linux",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Tags: "ts_omit_drive,ts_include_cli",
|
||||||
|
OnDep: func(dep string) {
|
||||||
|
if strings.Contains(dep, "driveimpl") {
|
||||||
|
t.Errorf("unexpected dep with ts_omit_drive: %q", dep)
|
||||||
|
}
|
||||||
|
if strings.Contains(dep, "webdav") {
|
||||||
|
t.Errorf("unexpected dep with ts_omit_drive: %q", dep)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}.Check(t)
|
||||||
|
}
|
||||||
|
@ -33,8 +33,8 @@ import (
|
|||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/cmd/tailscaled/childproc"
|
"tailscale.com/cmd/tailscaled/childproc"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/drive/driveimpl"
|
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
|
"tailscale.com/feature"
|
||||||
_ "tailscale.com/feature/condregister"
|
_ "tailscale.com/feature/condregister"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
@ -153,7 +153,6 @@ var subCommands = map[string]*func([]string) error{
|
|||||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||||
"debug": &debugModeFunc,
|
"debug": &debugModeFunc,
|
||||||
"be-child": &beChildFunc,
|
"be-child": &beChildFunc,
|
||||||
"serve-taildrive": &serveDriveFunc,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var beCLI func() // non-nil if CLI is linked in with the "ts_include_cli" build tag
|
var beCLI func() // non-nil if CLI is linked in with the "ts_include_cli" build tag
|
||||||
@ -480,7 +479,9 @@ func run() (err error) {
|
|||||||
debugMux = newDebugMux()
|
debugMux = newDebugMux()
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.Set(driveimpl.NewFileSystemForRemote(logf))
|
if f, ok := hookSetSysDrive.GetOk(); ok {
|
||||||
|
f(sys, logf)
|
||||||
|
}
|
||||||
|
|
||||||
if app := envknob.App(); app != "" {
|
if app := envknob.App(); app != "" {
|
||||||
hostinfo.SetApp(app)
|
hostinfo.SetApp(app)
|
||||||
@ -489,6 +490,11 @@ func run() (err error) {
|
|||||||
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hookSetSysDrive feature.Hook[func(*tsd.System, logger.Logf)]
|
||||||
|
hookSetWgEnginConfigDrive feature.Hook[func(*wgengine.Config, logger.Logf)]
|
||||||
|
)
|
||||||
|
|
||||||
var sigPipe os.Signal // set by sigpipe.go
|
var sigPipe os.Signal // set by sigpipe.go
|
||||||
|
|
||||||
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
|
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
|
||||||
@ -749,7 +755,9 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
|||||||
SetSubsystem: sys.Set,
|
SetSubsystem: sys.Set,
|
||||||
ControlKnobs: sys.ControlKnobs(),
|
ControlKnobs: sys.ControlKnobs(),
|
||||||
EventBus: sys.Bus.Get(),
|
EventBus: sys.Bus.Get(),
|
||||||
DriveForLocal: driveimpl.NewFileSystemForLocal(logf),
|
}
|
||||||
|
if f, ok := hookSetWgEnginConfigDrive.GetOk(); ok {
|
||||||
|
f(&conf, logf)
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
|
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
|
||||||
@ -943,35 +951,6 @@ func beChild(args []string) error {
|
|||||||
return f(args[1:])
|
return f(args[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
var serveDriveFunc = serveDrive
|
|
||||||
|
|
||||||
// serveDrive serves one or more Taildrives on localhost using the WebDAV
|
|
||||||
// protocol. On UNIX and MacOS tailscaled environment, Taildrive spawns child
|
|
||||||
// tailscaled processes in serve-taildrive mode in order to access the fliesystem
|
|
||||||
// as specific (usually unprivileged) users.
|
|
||||||
//
|
|
||||||
// serveDrive prints the address on which it's listening to stdout so that the
|
|
||||||
// parent process knows where to connect to.
|
|
||||||
func serveDrive(args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("missing shares")
|
|
||||||
}
|
|
||||||
if len(args)%2 != 0 {
|
|
||||||
return errors.New("need <sharename> <path> pairs")
|
|
||||||
}
|
|
||||||
s, err := driveimpl.NewFileServer()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to start Taildrive file server: %v", err)
|
|
||||||
}
|
|
||||||
shares := make(map[string]string)
|
|
||||||
for i := 0; i < len(args); i += 2 {
|
|
||||||
shares[args[i]] = args[i+1]
|
|
||||||
}
|
|
||||||
s.SetShares(shares)
|
|
||||||
fmt.Printf("%v\n", s.Addr())
|
|
||||||
return s.Serve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dieOnPipeReadErrorOfFD reads from the pipe named by fd and exit the process
|
// dieOnPipeReadErrorOfFD reads from the pipe named by fd and exit the process
|
||||||
// when the pipe becomes readable. We use this in tests as a somewhat more
|
// when the pipe becomes readable. We use this in tests as a somewhat more
|
||||||
// portable mechanism for the Linux PR_SET_PDEATHSIG, which we wish existed on
|
// portable mechanism for the Linux PR_SET_PDEATHSIG, which we wish existed on
|
||||||
|
56
cmd/tailscaled/tailscaled_drive.go
Normal file
56
cmd/tailscaled/tailscaled_drive.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_drive
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"tailscale.com/drive/driveimpl"
|
||||||
|
"tailscale.com/tsd"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/wgengine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
subCommands["serve-taildrive"] = &serveDriveFunc
|
||||||
|
|
||||||
|
hookSetSysDrive.Set(func(sys *tsd.System, logf logger.Logf) {
|
||||||
|
sys.Set(driveimpl.NewFileSystemForRemote(logf))
|
||||||
|
})
|
||||||
|
hookSetWgEnginConfigDrive.Set(func(conf *wgengine.Config, logf logger.Logf) {
|
||||||
|
conf.DriveForLocal = driveimpl.NewFileSystemForLocal(logf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var serveDriveFunc = serveDrive
|
||||||
|
|
||||||
|
// serveDrive serves one or more Taildrives on localhost using the WebDAV
|
||||||
|
// protocol. On UNIX and MacOS tailscaled environment, Taildrive spawns child
|
||||||
|
// tailscaled processes in serve-taildrive mode in order to access the fliesystem
|
||||||
|
// as specific (usually unprivileged) users.
|
||||||
|
//
|
||||||
|
// serveDrive prints the address on which it's listening to stdout so that the
|
||||||
|
// parent process knows where to connect to.
|
||||||
|
func serveDrive(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("missing shares")
|
||||||
|
}
|
||||||
|
if len(args)%2 != 0 {
|
||||||
|
return errors.New("need <sharename> <path> pairs")
|
||||||
|
}
|
||||||
|
s, err := driveimpl.NewFileServer()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to start Taildrive file server: %v", err)
|
||||||
|
}
|
||||||
|
shares := make(map[string]string)
|
||||||
|
for i := 0; i < len(args); i += 2 {
|
||||||
|
shares[args[i]] = args[i+1]
|
||||||
|
}
|
||||||
|
s.SetShares(shares)
|
||||||
|
fmt.Printf("%v\n", s.Addr())
|
||||||
|
return s.Serve()
|
||||||
|
}
|
8
feature/condregister/maybe_drive.go
Normal file
8
feature/condregister/maybe_drive.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_drive
|
||||||
|
|
||||||
|
package condregister
|
||||||
|
|
||||||
|
import _ "tailscale.com/feature/drive"
|
5
feature/drive/drive.go
Normal file
5
feature/drive/drive.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Package drive registers the Taildrive (file server) feature.
|
||||||
|
package drive
|
@ -1,38 +1,35 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_drive
|
||||||
|
|
||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
|
"tailscale.com/util/httpm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func init() {
|
||||||
// DriveLocalPort is the port on which the Taildrive listens for location
|
hookSetNetMapLockedDrive.Set(setNetMapLockedDrive)
|
||||||
// connections on quad 100.
|
|
||||||
DriveLocalPort = 8080
|
|
||||||
)
|
|
||||||
|
|
||||||
// DriveSharingEnabled reports whether sharing to remote nodes via Taildrive is
|
|
||||||
// enabled. This is currently based on checking for the drive:share node
|
|
||||||
// attribute.
|
|
||||||
func (b *LocalBackend) DriveSharingEnabled() bool {
|
|
||||||
return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveShare)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes
|
func setNetMapLockedDrive(b *LocalBackend, nm *netmap.NetworkMap) {
|
||||||
// is enabled. This is currently based on checking for the drive:access node
|
b.updateDrivePeersLocked(nm)
|
||||||
// attribute.
|
b.driveNotifyCurrentSharesLocked()
|
||||||
func (b *LocalBackend) DriveAccessEnabled() bool {
|
|
||||||
return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveAccess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DriveSetServerAddr tells Taildrive to use the given address for connecting
|
// DriveSetServerAddr tells Taildrive to use the given address for connecting
|
||||||
@ -363,3 +360,137 @@ func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Rem
|
|||||||
}
|
}
|
||||||
return driveRemotes
|
return driveRemotes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// responseBodyWrapper wraps an io.ReadCloser and stores
|
||||||
|
// the number of bytesRead.
|
||||||
|
type responseBodyWrapper struct {
|
||||||
|
io.ReadCloser
|
||||||
|
logVerbose bool
|
||||||
|
bytesRx int64
|
||||||
|
bytesTx int64
|
||||||
|
log logger.Logf
|
||||||
|
method string
|
||||||
|
statusCode int
|
||||||
|
contentType string
|
||||||
|
fileExtension string
|
||||||
|
shareNodeKey string
|
||||||
|
selfNodeKey string
|
||||||
|
contentLength int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// logAccess logs the taildrive: access: log line. If the logger is nil,
|
||||||
|
// the log will not be written.
|
||||||
|
func (rbw *responseBodyWrapper) logAccess(err string) {
|
||||||
|
if rbw.log == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some operating systems create and copy lots of 0 length hidden files for
|
||||||
|
// tracking various states. Omit these to keep logs from being too verbose.
|
||||||
|
if rbw.logVerbose || rbw.contentLength > 0 {
|
||||||
|
levelPrefix := ""
|
||||||
|
if rbw.logVerbose {
|
||||||
|
levelPrefix = "[v1] "
|
||||||
|
}
|
||||||
|
rbw.log(
|
||||||
|
"%staildrive: access: %s from %s to %s: status-code=%d ext=%q content-type=%q content-length=%.f tx=%.f rx=%.f err=%q",
|
||||||
|
levelPrefix,
|
||||||
|
rbw.method,
|
||||||
|
rbw.selfNodeKey,
|
||||||
|
rbw.shareNodeKey,
|
||||||
|
rbw.statusCode,
|
||||||
|
rbw.fileExtension,
|
||||||
|
rbw.contentType,
|
||||||
|
roundTraffic(rbw.contentLength),
|
||||||
|
roundTraffic(rbw.bytesTx), roundTraffic(rbw.bytesRx), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements the io.Reader interface.
|
||||||
|
func (rbw *responseBodyWrapper) Read(b []byte) (int, error) {
|
||||||
|
n, err := rbw.ReadCloser.Read(b)
|
||||||
|
rbw.bytesRx += int64(n)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
rbw.logAccess(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the io.Close interface.
|
||||||
|
func (rbw *responseBodyWrapper) Close() error {
|
||||||
|
err := rbw.ReadCloser.Close()
|
||||||
|
var errStr string
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
rbw.logAccess(errStr)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// driveTransport is an http.RoundTripper that wraps
|
||||||
|
// b.Dialer().PeerAPITransport() with metrics tracking.
|
||||||
|
type driveTransport struct {
|
||||||
|
b *LocalBackend
|
||||||
|
tr *http.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) newDriveTransport() *driveTransport {
|
||||||
|
return &driveTransport{
|
||||||
|
b: b,
|
||||||
|
tr: b.Dialer().PeerAPITransport(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *driveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
|
// Some WebDAV clients include origin and refer headers, which peerapi does
|
||||||
|
// not like. Remove them.
|
||||||
|
req.Header.Del("origin")
|
||||||
|
req.Header.Del("referer")
|
||||||
|
|
||||||
|
bw := &requestBodyWrapper{}
|
||||||
|
if req.Body != nil {
|
||||||
|
bw.ReadCloser = req.Body
|
||||||
|
req.Body = bw
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
contentType := "unknown"
|
||||||
|
if ct := req.Header.Get("Content-Type"); ct != "" {
|
||||||
|
contentType = ct
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.b.mu.Lock()
|
||||||
|
selfNodeKey := dt.b.currentNode().Self().Key().ShortString()
|
||||||
|
dt.b.mu.Unlock()
|
||||||
|
n, _, ok := dt.b.WhoIs("tcp", netip.MustParseAddrPort(req.URL.Host))
|
||||||
|
shareNodeKey := "unknown"
|
||||||
|
if ok {
|
||||||
|
shareNodeKey = string(n.Key().ShortString())
|
||||||
|
}
|
||||||
|
|
||||||
|
rbw := responseBodyWrapper{
|
||||||
|
log: dt.b.logf,
|
||||||
|
logVerbose: req.Method != httpm.GET && req.Method != httpm.PUT, // other requests like PROPFIND are quite chatty, so we log those at verbose level
|
||||||
|
method: req.Method,
|
||||||
|
bytesTx: int64(bw.bytesRead),
|
||||||
|
selfNodeKey: selfNodeKey,
|
||||||
|
shareNodeKey: shareNodeKey,
|
||||||
|
contentType: contentType,
|
||||||
|
contentLength: resp.ContentLength,
|
||||||
|
fileExtension: parseDriveFileExtensionForLog(req.URL.Path),
|
||||||
|
statusCode: resp.StatusCode,
|
||||||
|
ReadCloser: resp.Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
// in case of error response, just log immediately
|
||||||
|
rbw.logAccess("")
|
||||||
|
} else {
|
||||||
|
resp.Body = &rbw
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return dt.tr.RoundTrip(req)
|
||||||
|
}
|
||||||
|
30
ipn/ipnlocal/drive_tomove.go
Normal file
30
ipn/ipnlocal/drive_tomove.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// This is the Taildrive stuff that should ideally be registered in init only when
|
||||||
|
// the ts_omit_drive is not set, but for transition reasons is currently (2025-09-08)
|
||||||
|
// always defined, as we work to pull it out of LocalBackend.
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import "tailscale.com/tailcfg"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DriveLocalPort is the port on which the Taildrive listens for location
|
||||||
|
// connections on quad 100.
|
||||||
|
DriveLocalPort = 8080
|
||||||
|
)
|
||||||
|
|
||||||
|
// DriveSharingEnabled reports whether sharing to remote nodes via Taildrive is
|
||||||
|
// enabled. This is currently based on checking for the drive:share node
|
||||||
|
// attribute.
|
||||||
|
func (b *LocalBackend) DriveSharingEnabled() bool {
|
||||||
|
return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveShare)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes
|
||||||
|
// is enabled. This is currently based on checking for the drive:access node
|
||||||
|
// attribute.
|
||||||
|
func (b *LocalBackend) DriveAccessEnabled() bool {
|
||||||
|
return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveAccess)
|
||||||
|
}
|
@ -52,6 +52,7 @@ import (
|
|||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/envknob/featureknob"
|
"tailscale.com/envknob/featureknob"
|
||||||
|
"tailscale.com/feature"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/health/healthmsg"
|
"tailscale.com/health/healthmsg"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
@ -100,7 +101,6 @@ import (
|
|||||||
"tailscale.com/util/deephash"
|
"tailscale.com/util/deephash"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
"tailscale.com/util/goroutines"
|
"tailscale.com/util/goroutines"
|
||||||
"tailscale.com/util/httpm"
|
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
"tailscale.com/util/multierr"
|
"tailscale.com/util/multierr"
|
||||||
"tailscale.com/util/osuser"
|
"tailscale.com/util/osuser"
|
||||||
@ -6326,143 +6326,12 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
|||||||
b.metrics.approvedRoutes.Set(approved)
|
b.metrics.approvedRoutes.Set(approved)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.updateDrivePeersLocked(nm)
|
if f, ok := hookSetNetMapLockedDrive.GetOk(); ok {
|
||||||
b.driveNotifyCurrentSharesLocked()
|
f(b, nm)
|
||||||
}
|
|
||||||
|
|
||||||
// responseBodyWrapper wraps an io.ReadCloser and stores
|
|
||||||
// the number of bytesRead.
|
|
||||||
type responseBodyWrapper struct {
|
|
||||||
io.ReadCloser
|
|
||||||
logVerbose bool
|
|
||||||
bytesRx int64
|
|
||||||
bytesTx int64
|
|
||||||
log logger.Logf
|
|
||||||
method string
|
|
||||||
statusCode int
|
|
||||||
contentType string
|
|
||||||
fileExtension string
|
|
||||||
shareNodeKey string
|
|
||||||
selfNodeKey string
|
|
||||||
contentLength int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// logAccess logs the taildrive: access: log line. If the logger is nil,
|
|
||||||
// the log will not be written.
|
|
||||||
func (rbw *responseBodyWrapper) logAccess(err string) {
|
|
||||||
if rbw.log == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some operating systems create and copy lots of 0 length hidden files for
|
|
||||||
// tracking various states. Omit these to keep logs from being too verbose.
|
|
||||||
if rbw.logVerbose || rbw.contentLength > 0 {
|
|
||||||
levelPrefix := ""
|
|
||||||
if rbw.logVerbose {
|
|
||||||
levelPrefix = "[v1] "
|
|
||||||
}
|
|
||||||
rbw.log(
|
|
||||||
"%staildrive: access: %s from %s to %s: status-code=%d ext=%q content-type=%q content-length=%.f tx=%.f rx=%.f err=%q",
|
|
||||||
levelPrefix,
|
|
||||||
rbw.method,
|
|
||||||
rbw.selfNodeKey,
|
|
||||||
rbw.shareNodeKey,
|
|
||||||
rbw.statusCode,
|
|
||||||
rbw.fileExtension,
|
|
||||||
rbw.contentType,
|
|
||||||
roundTraffic(rbw.contentLength),
|
|
||||||
roundTraffic(rbw.bytesTx), roundTraffic(rbw.bytesRx), err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read implements the io.Reader interface.
|
var hookSetNetMapLockedDrive feature.Hook[func(*LocalBackend, *netmap.NetworkMap)]
|
||||||
func (rbw *responseBodyWrapper) Read(b []byte) (int, error) {
|
|
||||||
n, err := rbw.ReadCloser.Read(b)
|
|
||||||
rbw.bytesRx += int64(n)
|
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
rbw.logAccess(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements the io.Close interface.
|
|
||||||
func (rbw *responseBodyWrapper) Close() error {
|
|
||||||
err := rbw.ReadCloser.Close()
|
|
||||||
var errStr string
|
|
||||||
if err != nil {
|
|
||||||
errStr = err.Error()
|
|
||||||
}
|
|
||||||
rbw.logAccess(errStr)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// driveTransport is an http.RoundTripper that wraps
|
|
||||||
// b.Dialer().PeerAPITransport() with metrics tracking.
|
|
||||||
type driveTransport struct {
|
|
||||||
b *LocalBackend
|
|
||||||
tr *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *LocalBackend) newDriveTransport() *driveTransport {
|
|
||||||
return &driveTransport{
|
|
||||||
b: b,
|
|
||||||
tr: b.Dialer().PeerAPITransport(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dt *driveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
|
||||||
// Some WebDAV clients include origin and refer headers, which peerapi does
|
|
||||||
// not like. Remove them.
|
|
||||||
req.Header.Del("origin")
|
|
||||||
req.Header.Del("referer")
|
|
||||||
|
|
||||||
bw := &requestBodyWrapper{}
|
|
||||||
if req.Body != nil {
|
|
||||||
bw.ReadCloser = req.Body
|
|
||||||
req.Body = bw
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
contentType := "unknown"
|
|
||||||
if ct := req.Header.Get("Content-Type"); ct != "" {
|
|
||||||
contentType = ct
|
|
||||||
}
|
|
||||||
|
|
||||||
dt.b.mu.Lock()
|
|
||||||
selfNodeKey := dt.b.currentNode().Self().Key().ShortString()
|
|
||||||
dt.b.mu.Unlock()
|
|
||||||
n, _, ok := dt.b.WhoIs("tcp", netip.MustParseAddrPort(req.URL.Host))
|
|
||||||
shareNodeKey := "unknown"
|
|
||||||
if ok {
|
|
||||||
shareNodeKey = string(n.Key().ShortString())
|
|
||||||
}
|
|
||||||
|
|
||||||
rbw := responseBodyWrapper{
|
|
||||||
log: dt.b.logf,
|
|
||||||
logVerbose: req.Method != httpm.GET && req.Method != httpm.PUT, // other requests like PROPFIND are quite chatty, so we log those at verbose level
|
|
||||||
method: req.Method,
|
|
||||||
bytesTx: int64(bw.bytesRead),
|
|
||||||
selfNodeKey: selfNodeKey,
|
|
||||||
shareNodeKey: shareNodeKey,
|
|
||||||
contentType: contentType,
|
|
||||||
contentLength: resp.ContentLength,
|
|
||||||
fileExtension: parseDriveFileExtensionForLog(req.URL.Path),
|
|
||||||
statusCode: resp.StatusCode,
|
|
||||||
ReadCloser: resp.Body,
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
// in case of error response, just log immediately
|
|
||||||
rbw.logAccess("")
|
|
||||||
} else {
|
|
||||||
resp.Body = &rbw
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return dt.tr.RoundTrip(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// roundTraffic rounds bytes. This is used to preserve user privacy within logs.
|
// roundTraffic rounds bytes. This is used to preserve user privacy within logs.
|
||||||
func roundTraffic(bytes int64) float64 {
|
func roundTraffic(bytes int64) float64 {
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -26,7 +25,6 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/net/dns/dnsmessage"
|
"golang.org/x/net/dns/dnsmessage"
|
||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
"tailscale.com/drive"
|
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
@ -39,14 +37,9 @@ import (
|
|||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
"tailscale.com/util/httpm"
|
|
||||||
"tailscale.com/wgengine/filter"
|
"tailscale.com/wgengine/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
taildrivePrefix = "/v0/drive"
|
|
||||||
)
|
|
||||||
|
|
||||||
var initListenConfig func(*net.ListenConfig, netip.Addr, *netmon.State, string) error
|
var initListenConfig func(*net.ListenConfig, netip.Addr, *netmon.State, string) error
|
||||||
|
|
||||||
// addH2C is non-nil on platforms where we want to add H2C
|
// addH2C is non-nil on platforms where we want to add H2C
|
||||||
@ -369,10 +362,6 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.handleDNSQuery(w, r)
|
h.handleDNSQuery(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(r.URL.Path, taildrivePrefix) {
|
|
||||||
h.handleServeDrive(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/v0/goroutines":
|
case "/v0/goroutines":
|
||||||
h.handleServeGoroutines(w, r)
|
h.handleServeGoroutines(w, r)
|
||||||
@ -1018,90 +1007,6 @@ func (rbw *requestBodyWrapper) Read(b []byte) (int, error) {
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *peerAPIHandler) handleServeDrive(w http.ResponseWriter, r *http.Request) {
|
|
||||||
h.logfv1("taildrive: got %s request from %s", r.Method, h.peerNode.Key().ShortString())
|
|
||||||
if !h.ps.b.DriveSharingEnabled() {
|
|
||||||
h.logf("taildrive: not enabled")
|
|
||||||
http.Error(w, "taildrive not enabled", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
capsMap := h.PeerCaps()
|
|
||||||
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive]
|
|
||||||
if !ok {
|
|
||||||
h.logf("taildrive: not permitted")
|
|
||||||
http.Error(w, "taildrive not permitted", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rawPerms := make([][]byte, 0, len(driveCaps))
|
|
||||||
for _, cap := range driveCaps {
|
|
||||||
rawPerms = append(rawPerms, []byte(cap))
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := drive.ParsePermissions(rawPerms)
|
|
||||||
if err != nil {
|
|
||||||
h.logf("taildrive: error parsing permissions: %v", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fs, ok := h.ps.b.sys.DriveForRemote.GetOK()
|
|
||||||
if !ok {
|
|
||||||
h.logf("taildrive: not supported on platform")
|
|
||||||
http.Error(w, "taildrive not supported on platform", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wr := &httpResponseWrapper{
|
|
||||||
ResponseWriter: w,
|
|
||||||
}
|
|
||||||
bw := &requestBodyWrapper{
|
|
||||||
ReadCloser: r.Body,
|
|
||||||
}
|
|
||||||
r.Body = bw
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
switch wr.statusCode {
|
|
||||||
case 304:
|
|
||||||
// 304s are particularly chatty so skip logging.
|
|
||||||
default:
|
|
||||||
log := h.logf
|
|
||||||
if r.Method != httpm.PUT && r.Method != httpm.GET {
|
|
||||||
log = h.logfv1
|
|
||||||
}
|
|
||||||
contentType := "unknown"
|
|
||||||
if ct := wr.Header().Get("Content-Type"); ct != "" {
|
|
||||||
contentType = ct
|
|
||||||
}
|
|
||||||
|
|
||||||
log("taildrive: share: %s from %s to %s: status-code=%d ext=%q content-type=%q tx=%.f rx=%.f", r.Method, h.peerNode.Key().ShortString(), h.selfNode.Key().ShortString(), wr.statusCode, parseDriveFileExtensionForLog(r.URL.Path), contentType, roundTraffic(wr.contentLength), roundTraffic(bw.bytesRead))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, taildrivePrefix)
|
|
||||||
fs.ServeHTTPWithPerms(p, wr, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseDriveFileExtensionForLog parses the file extension, if available.
|
|
||||||
// If a file extension is not present or parsable, the file extension is
|
|
||||||
// set to "unknown". If the file extension contains a double quote, it is
|
|
||||||
// replaced with "removed".
|
|
||||||
// All whitespace is removed from a parsed file extension.
|
|
||||||
// File extensions including the leading ., e.g. ".gif".
|
|
||||||
func parseDriveFileExtensionForLog(path string) string {
|
|
||||||
fileExt := "unknown"
|
|
||||||
if fe := filepath.Ext(path); fe != "" {
|
|
||||||
if strings.Contains(fe, "\"") {
|
|
||||||
// Do not log include file extensions with quotes within them.
|
|
||||||
return "removed"
|
|
||||||
}
|
|
||||||
// Remove white space from user defined inputs.
|
|
||||||
fileExt = strings.ReplaceAll(fe, " ", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileExt
|
|
||||||
}
|
|
||||||
|
|
||||||
// peerAPIURL returns an HTTP URL for the peer's peerapi service,
|
// peerAPIURL returns an HTTP URL for the peer's peerapi service,
|
||||||
// without a trailing slash.
|
// without a trailing slash.
|
||||||
//
|
//
|
||||||
|
110
ipn/ipnlocal/peerapi_drive.go
Normal file
110
ipn/ipnlocal/peerapi_drive.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_drive
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tailscale.com/drive"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/util/httpm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
taildrivePrefix = "/v0/drive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
peerAPIHandlerPrefixes[taildrivePrefix] = handleServeDrive
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServeDrive(hi PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
|
||||||
|
h := hi.(*peerAPIHandler)
|
||||||
|
|
||||||
|
h.logfv1("taildrive: got %s request from %s", r.Method, h.peerNode.Key().ShortString())
|
||||||
|
if !h.ps.b.DriveSharingEnabled() {
|
||||||
|
h.logf("taildrive: not enabled")
|
||||||
|
http.Error(w, "taildrive not enabled", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
capsMap := h.PeerCaps()
|
||||||
|
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive]
|
||||||
|
if !ok {
|
||||||
|
h.logf("taildrive: not permitted")
|
||||||
|
http.Error(w, "taildrive not permitted", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPerms := make([][]byte, 0, len(driveCaps))
|
||||||
|
for _, cap := range driveCaps {
|
||||||
|
rawPerms = append(rawPerms, []byte(cap))
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := drive.ParsePermissions(rawPerms)
|
||||||
|
if err != nil {
|
||||||
|
h.logf("taildrive: error parsing permissions: %v", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fs, ok := h.ps.b.sys.DriveForRemote.GetOK()
|
||||||
|
if !ok {
|
||||||
|
h.logf("taildrive: not supported on platform")
|
||||||
|
http.Error(w, "taildrive not supported on platform", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wr := &httpResponseWrapper{
|
||||||
|
ResponseWriter: w,
|
||||||
|
}
|
||||||
|
bw := &requestBodyWrapper{
|
||||||
|
ReadCloser: r.Body,
|
||||||
|
}
|
||||||
|
r.Body = bw
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
switch wr.statusCode {
|
||||||
|
case 304:
|
||||||
|
// 304s are particularly chatty so skip logging.
|
||||||
|
default:
|
||||||
|
log := h.logf
|
||||||
|
if r.Method != httpm.PUT && r.Method != httpm.GET {
|
||||||
|
log = h.logfv1
|
||||||
|
}
|
||||||
|
contentType := "unknown"
|
||||||
|
if ct := wr.Header().Get("Content-Type"); ct != "" {
|
||||||
|
contentType = ct
|
||||||
|
}
|
||||||
|
|
||||||
|
log("taildrive: share: %s from %s to %s: status-code=%d ext=%q content-type=%q tx=%.f rx=%.f", r.Method, h.peerNode.Key().ShortString(), h.selfNode.Key().ShortString(), wr.statusCode, parseDriveFileExtensionForLog(r.URL.Path), contentType, roundTraffic(wr.contentLength), roundTraffic(bw.bytesRead))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, taildrivePrefix)
|
||||||
|
fs.ServeHTTPWithPerms(p, wr, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDriveFileExtensionForLog parses the file extension, if available.
|
||||||
|
// If a file extension is not present or parsable, the file extension is
|
||||||
|
// set to "unknown". If the file extension contains a double quote, it is
|
||||||
|
// replaced with "removed".
|
||||||
|
// All whitespace is removed from a parsed file extension.
|
||||||
|
// File extensions including the leading ., e.g. ".gif".
|
||||||
|
func parseDriveFileExtensionForLog(path string) string {
|
||||||
|
fileExt := "unknown"
|
||||||
|
if fe := filepath.Ext(path); fe != "" {
|
||||||
|
if strings.Contains(fe, "\"") {
|
||||||
|
// Do not log include file extensions with quotes within them.
|
||||||
|
return "removed"
|
||||||
|
}
|
||||||
|
// Remove white space from user defined inputs.
|
||||||
|
fileExt = strings.ReplaceAll(fe, " ", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileExt
|
||||||
|
}
|
@ -18,8 +18,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
@ -31,7 +29,6 @@ import (
|
|||||||
"golang.org/x/net/dns/dnsmessage"
|
"golang.org/x/net/dns/dnsmessage"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/clientupdate"
|
"tailscale.com/clientupdate"
|
||||||
"tailscale.com/drive"
|
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health/healthmsg"
|
"tailscale.com/health/healthmsg"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
@ -104,8 +101,6 @@ var handler = map[string]LocalAPIHandler{
|
|||||||
"disconnect-control": (*Handler).disconnectControl,
|
"disconnect-control": (*Handler).disconnectControl,
|
||||||
"dns-osconfig": (*Handler).serveDNSOSConfig,
|
"dns-osconfig": (*Handler).serveDNSOSConfig,
|
||||||
"dns-query": (*Handler).serveDNSQuery,
|
"dns-query": (*Handler).serveDNSQuery,
|
||||||
"drive/fileserver-address": (*Handler).serveDriveServerAddr,
|
|
||||||
"drive/shares": (*Handler).serveShares,
|
|
||||||
"goroutines": (*Handler).serveGoroutines,
|
"goroutines": (*Handler).serveGoroutines,
|
||||||
"handle-push-message": (*Handler).serveHandlePushMessage,
|
"handle-push-message": (*Handler).serveHandlePushMessage,
|
||||||
"id-token": (*Handler).serveIDToken,
|
"id-token": (*Handler).serveIDToken,
|
||||||
@ -2661,124 +2656,6 @@ func (h *Handler) serveDNSQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveDriveServerAddr handles updates of the Taildrive file server address.
|
|
||||||
func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != httpm.PUT {
|
|
||||||
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.b.DriveSetServerAddr(string(b))
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveShares handles the management of Taildrive shares.
|
|
||||||
//
|
|
||||||
// PUT - adds or updates an existing share
|
|
||||||
// DELETE - removes a share
|
|
||||||
// GET - gets a list of all shares, sorted by name
|
|
||||||
// POST - renames an existing share
|
|
||||||
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.b.DriveSharingEnabled() {
|
|
||||||
http.Error(w, `taildrive sharing not enabled, please add the attribute "drive:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch r.Method {
|
|
||||||
case httpm.PUT:
|
|
||||||
var share drive.Share
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&share)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
share.Path = path.Clean(share.Path)
|
|
||||||
fi, err := os.Stat(share.Path)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !fi.IsDir() {
|
|
||||||
http.Error(w, "not a directory", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if drive.AllowShareAs() {
|
|
||||||
// share as the connected user
|
|
||||||
username, err := h.Actor.Username()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
share.As = username
|
|
||||||
}
|
|
||||||
err = h.b.DriveSetShare(&share)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, drive.ErrInvalidShareName) {
|
|
||||||
http.Error(w, "invalid share name", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
case httpm.DELETE:
|
|
||||||
b, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = h.b.DriveRemoveShare(string(b))
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
http.Error(w, "share not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
case httpm.POST:
|
|
||||||
var names [2]string
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&names)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = h.b.DriveRenameShare(names[0], names[1])
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
http.Error(w, "share not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if os.IsExist(err) {
|
|
||||||
http.Error(w, "share name already used", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.Is(err, drive.ErrInvalidShareName) {
|
|
||||||
http.Error(w, "invalid share name", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
case httpm.GET:
|
|
||||||
shares := h.b.DriveGetShares()
|
|
||||||
err := json.NewEncoder(w).Encode(shares)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveSuggestExitNode serves a POST endpoint for returning a suggested exit node.
|
// serveSuggestExitNode serves a POST endpoint for returning a suggested exit node.
|
||||||
func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != httpm.GET {
|
if r.Method != httpm.GET {
|
||||||
|
141
ipn/localapi/localapi_drive.go
Normal file
141
ipn/localapi/localapi_drive.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_drive
|
||||||
|
|
||||||
|
package localapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"tailscale.com/drive"
|
||||||
|
"tailscale.com/util/httpm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("drive/fileserver-address", (*Handler).serveDriveServerAddr)
|
||||||
|
Register("drive/shares", (*Handler).serveShares)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveDriveServerAddr handles updates of the Taildrive file server address.
|
||||||
|
func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != httpm.PUT {
|
||||||
|
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.b.DriveSetServerAddr(string(b))
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveShares handles the management of Taildrive shares.
|
||||||
|
//
|
||||||
|
// PUT - adds or updates an existing share
|
||||||
|
// DELETE - removes a share
|
||||||
|
// GET - gets a list of all shares, sorted by name
|
||||||
|
// POST - renames an existing share
|
||||||
|
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.b.DriveSharingEnabled() {
|
||||||
|
http.Error(w, `taildrive sharing not enabled, please add the attribute "drive:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch r.Method {
|
||||||
|
case httpm.PUT:
|
||||||
|
var share drive.Share
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&share)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
share.Path = path.Clean(share.Path)
|
||||||
|
fi, err := os.Stat(share.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fi.IsDir() {
|
||||||
|
http.Error(w, "not a directory", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if drive.AllowShareAs() {
|
||||||
|
// share as the connected user
|
||||||
|
username, err := h.Actor.Username()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
share.As = username
|
||||||
|
}
|
||||||
|
err = h.b.DriveSetShare(&share)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, drive.ErrInvalidShareName) {
|
||||||
|
http.Error(w, "invalid share name", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
case httpm.DELETE:
|
||||||
|
b, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = h.b.DriveRemoveShare(string(b))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
http.Error(w, "share not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
case httpm.POST:
|
||||||
|
var names [2]string
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&names)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = h.b.DriveRenameShare(names[0], names[1])
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
http.Error(w, "share not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if os.IsExist(err) {
|
||||||
|
http.Error(w, "share name already used", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, drive.ErrInvalidShareName) {
|
||||||
|
http.Error(w, "invalid share name", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
case httpm.GET:
|
||||||
|
shares := h.b.DriveGetShares()
|
||||||
|
err := json.NewEncoder(w).Encode(shares)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import (
|
|||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/drive/driveimpl"
|
_ "tailscale.com/drive/driveimpl"
|
||||||
_ "tailscale.com/envknob"
|
_ "tailscale.com/envknob"
|
||||||
|
_ "tailscale.com/feature"
|
||||||
_ "tailscale.com/feature/condregister"
|
_ "tailscale.com/feature/condregister"
|
||||||
_ "tailscale.com/health"
|
_ "tailscale.com/health"
|
||||||
_ "tailscale.com/hostinfo"
|
_ "tailscale.com/hostinfo"
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/drive/driveimpl"
|
_ "tailscale.com/drive/driveimpl"
|
||||||
_ "tailscale.com/envknob"
|
_ "tailscale.com/envknob"
|
||||||
|
_ "tailscale.com/feature"
|
||||||
_ "tailscale.com/feature/condregister"
|
_ "tailscale.com/feature/condregister"
|
||||||
_ "tailscale.com/health"
|
_ "tailscale.com/health"
|
||||||
_ "tailscale.com/hostinfo"
|
_ "tailscale.com/hostinfo"
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/drive/driveimpl"
|
_ "tailscale.com/drive/driveimpl"
|
||||||
_ "tailscale.com/envknob"
|
_ "tailscale.com/envknob"
|
||||||
|
_ "tailscale.com/feature"
|
||||||
_ "tailscale.com/feature/condregister"
|
_ "tailscale.com/feature/condregister"
|
||||||
_ "tailscale.com/health"
|
_ "tailscale.com/health"
|
||||||
_ "tailscale.com/hostinfo"
|
_ "tailscale.com/hostinfo"
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/drive/driveimpl"
|
_ "tailscale.com/drive/driveimpl"
|
||||||
_ "tailscale.com/envknob"
|
_ "tailscale.com/envknob"
|
||||||
|
_ "tailscale.com/feature"
|
||||||
_ "tailscale.com/feature/condregister"
|
_ "tailscale.com/feature/condregister"
|
||||||
_ "tailscale.com/health"
|
_ "tailscale.com/health"
|
||||||
_ "tailscale.com/hostinfo"
|
_ "tailscale.com/hostinfo"
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/drive/driveimpl"
|
_ "tailscale.com/drive/driveimpl"
|
||||||
_ "tailscale.com/envknob"
|
_ "tailscale.com/envknob"
|
||||||
|
_ "tailscale.com/feature"
|
||||||
_ "tailscale.com/feature/condregister"
|
_ "tailscale.com/feature/condregister"
|
||||||
_ "tailscale.com/health"
|
_ "tailscale.com/health"
|
||||||
_ "tailscale.com/hostinfo"
|
_ "tailscale.com/hostinfo"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user