~stepbrobd/tailscale

993acf4475b22d693f210ae08cd8de57e2452d28 — Percy Wegmann 10 months ago 2e404b7
tailfs: initial implementation

Add a WebDAV-based folder sharing mechanism that is exposed to local clients at
100.100.100.100:8080 and to remote peers via a new peerapi endpoint at
/v0/tailfs.

Add the ability to manage folder sharing via the new 'share' CLI sub-command.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
61 files changed, 4919 insertions(+), 284 deletions(-)

M client/tailscale/localclient.go
M cmd/derper/depaware.txt
M cmd/tailscale/cli/cli.go
A cmd/tailscale/cli/share.go
M cmd/tailscale/depaware.txt
M cmd/tailscaled/depaware.txt
M cmd/tailscaled/tailscaled.go
M cmd/tsconnect/wasm/wasm_js.go
M flake.nix
M go.mod
M go.mod.sri
M go.sum
M ipn/backend.go
M ipn/ipnlocal/local.go
M ipn/ipnlocal/local_test.go
M ipn/ipnlocal/peerapi.go
M ipn/ipnlocal/serve.go
A ipn/ipnlocal/tailfs.go
A ipn/ipnlocal/tailfs_test.go
M ipn/localapi/localapi.go
M shell.nix
M tailcfg/tailcfg.go
A tailfs/birthtiming.go
A tailfs/birthtiming_test.go
A tailfs/compositefs/compositefs.go
A tailfs/compositefs/compositefs_test.go
A tailfs/compositefs/mkdir.go
A tailfs/compositefs/openfile.go
A tailfs/compositefs/removeall.go
A tailfs/compositefs/rename.go
A tailfs/compositefs/stat.go
A tailfs/connlistener.go
A tailfs/connlistener_test.go
A tailfs/fileserver.go
A tailfs/local.go
A tailfs/remote.go
A tailfs/remote_nonunix.go
A tailfs/remote_permissions.go
A tailfs/remote_permissions_test.go
A tailfs/remote_unix.go
A tailfs/shared/pathutil.go
A tailfs/shared/pathutil_test.go
A tailfs/shared/readonlydir.go
A tailfs/shared/stat.go
A tailfs/tailfs.go
A tailfs/tailfs_test.go
A tailfs/webdavfs/readonly_file.go
A tailfs/webdavfs/stat_cache.go
A tailfs/webdavfs/stat_cache_test.go
A tailfs/webdavfs/webdavfs.go
A tailfs/webdavfs/writeonly_file.go
M tsd/tsd.go
M tsnet/tsnet.go
M tstest/integration/tailscaled_deps_test_darwin.go
M tstest/integration/tailscaled_deps_test_freebsd.go
M tstest/integration/tailscaled_deps_test_linux.go
M tstest/integration/tailscaled_deps_test_openbsd.go
M tstest/integration/tailscaled_deps_test_windows.go
M wgengine/netstack/netstack.go
M wgengine/netstack/netstack_test.go
M wgengine/userspace.go
M client/tailscale/localclient.go => client/tailscale/localclient.go +43 -0
@@ 35,6 35,7 @@ import (
	"tailscale.com/paths"
	"tailscale.com/safesocket"
	"tailscale.com/tailcfg"
	"tailscale.com/tailfs"
	"tailscale.com/tka"
	"tailscale.com/types/key"
	"tailscale.com/types/tkatype"


@@ 1417,6 1418,48 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
	return &cv, nil
}

// TailfsSetFileServerAddr instructs Tailfs to use the server at addr to access
// the filesystem. This is used on platforms like Windows and MacOS to let
// Tailfs know to use the file server running in the GUI app.
func (lc *LocalClient) TailfsSetFileServerAddr(ctx context.Context, addr string) error {
	_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
	return err
}

// TailfsShareAdd adds the given share to the list of shares that Tailfs will
// serve to remote nodes. If a share with the same name already exists, the
// existing share is replaced/updated.
func (lc *LocalClient) TailfsShareAdd(ctx context.Context, share *tailfs.Share) error {
	_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
	return err
}

// TailfsShareRemove removes the share with the given name from the list of
// shares that Tailfs will serve to remote nodes.
func (lc *LocalClient) TailfsShareRemove(ctx context.Context, name string) error {
	_, err := lc.send(
		ctx,
		"DELETE",
		"/localapi/v0/tailfs/shares",
		http.StatusNoContent,
		jsonBody(&tailfs.Share{
			Name: name,
		}))
	return err
}

// TailfsShareList returns the list of shares that Tailfs is currently serving
// to remote nodes.
func (lc *LocalClient) TailfsShareList(ctx context.Context) (map[string]*tailfs.Share, error) {
	result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
	if err != nil {
		return nil, err
	}
	var shares map[string]*tailfs.Share
	err = json.Unmarshal(result, &shares)
	return shares, err
}

// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
// It's returned by LocalClient.WatchIPNBus.
//

M cmd/derper/depaware.txt => cmd/derper/depaware.txt +42 -30
@@ 9,6 9,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
     💣 github.com/cespare/xxhash/v2                                 from github.com/prometheus/client_golang/prometheus
   L    github.com/coreos/go-iptables/iptables                       from tailscale.com/util/linuxfw
   W 💣 github.com/dblohm7/wingoes                                   from tailscale.com/util/winutil
     💣 github.com/djherbis/times                                    from tailscale.com/tailfs
        github.com/fxamacker/cbor/v2                                 from tailscale.com/tka
        github.com/golang/groupcache/lru                             from tailscale.com/net/dnscache
   L    github.com/google/nftables                                   from tailscale.com/util/linuxfw


@@ 19,10 20,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
   L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
        github.com/google/uuid                                       from tailscale.com/tsweb
        github.com/hdevalence/ed25519consensus                       from tailscale.com/tka
        github.com/jellydator/ttlcache/v3                            from tailscale.com/tailfs/webdavfs
   L    github.com/josharian/native                                  from github.com/mdlayher/netlink+
   L 💣 github.com/jsimonetti/rtnetlink                              from tailscale.com/net/interfaces+
   L    github.com/jsimonetti/rtnetlink/internal/unix                from github.com/jsimonetti/rtnetlink
   L 💣 github.com/mdlayher/netlink                                  from github.com/jsimonetti/rtnetlink+
   L 💣 github.com/mdlayher/netlink                                  from github.com/google/nftables+
   L 💣 github.com/mdlayher/netlink/nlenc                            from github.com/jsimonetti/rtnetlink+
   L    github.com/mdlayher/netlink/nltest                           from github.com/google/nftables
   L 💣 github.com/mdlayher/socket                                   from github.com/mdlayher/netlink


@@ 41,12 43,15 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
   W 💣 github.com/tailscale/go-winio/internal/socket                from github.com/tailscale/go-winio
   W    github.com/tailscale/go-winio/internal/stringbuffer          from github.com/tailscale/go-winio/internal/fs
   W    github.com/tailscale/go-winio/pkg/guid                       from github.com/tailscale/go-winio+
        github.com/tailscale/gowebdav                                from tailscale.com/tailfs/webdavfs
   L 💣 github.com/tailscale/netlink                                 from tailscale.com/util/linuxfw
        github.com/tailscale/xnet/webdav                             from tailscale.com/tailfs+
        github.com/tailscale/xnet/webdav/internal/xml                from github.com/tailscale/xnet/webdav
   L 💣 github.com/vishvananda/netlink/nl                            from github.com/tailscale/netlink
   L    github.com/vishvananda/netns                                 from github.com/tailscale/netlink+
        github.com/x448/float16                                      from github.com/fxamacker/cbor/v2
     💣 go4.org/mem                                                  from tailscale.com/client/tailscale+
        go4.org/netipx                                               from tailscale.com/wgengine/filter+
        go4.org/netipx                                               from tailscale.com/net/tsaddr+
   W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg           from tailscale.com/net/interfaces+
        google.golang.org/protobuf/encoding/protodelim               from github.com/prometheus/common/expfmt
        google.golang.org/protobuf/encoding/prototext                from github.com/prometheus/common/expfmt+


@@ 86,7 91,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        tailscale.com/derp                                           from tailscale.com/cmd/derper+
        tailscale.com/derp/derphttp                                  from tailscale.com/cmd/derper
        tailscale.com/disco                                          from tailscale.com/derp
        tailscale.com/envknob                                        from tailscale.com/derp+
        tailscale.com/envknob                                        from tailscale.com/client/tailscale+
        tailscale.com/health                                         from tailscale.com/net/tlsdial
        tailscale.com/hostinfo                                       from tailscale.com/net/interfaces+
        tailscale.com/ipn                                            from tailscale.com/client/tailscale


@@ 94,10 99,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        tailscale.com/metrics                                        from tailscale.com/cmd/derper+
        tailscale.com/net/dnscache                                   from tailscale.com/derp/derphttp
        tailscale.com/net/flowtrack                                  from tailscale.com/net/packet+
     💣 tailscale.com/net/interfaces                                 from tailscale.com/net/netns+
     💣 tailscale.com/net/interfaces                                 from tailscale.com/net/netmon+
        tailscale.com/net/netaddr                                    from tailscale.com/ipn+
        tailscale.com/net/netknob                                    from tailscale.com/net/netns
        tailscale.com/net/netmon                                     from tailscale.com/net/sockstats+
        tailscale.com/net/netmon                                     from tailscale.com/derp/derphttp+
        tailscale.com/net/netns                                      from tailscale.com/derp/derphttp
        tailscale.com/net/netutil                                    from tailscale.com/client/tailscale
        tailscale.com/net/packet                                     from tailscale.com/wgengine/filter


@@ 110,21 115,25 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
     💣 tailscale.com/net/tshttpproxy                                from tailscale.com/derp/derphttp+
        tailscale.com/net/wsconn                                     from tailscale.com/cmd/derper+
        tailscale.com/paths                                          from tailscale.com/client/tailscale
     💣 tailscale.com/safesocket                                     from tailscale.com/client/tailscale
     💣 tailscale.com/safesocket                                     from tailscale.com/client/tailscale+
        tailscale.com/syncs                                          from tailscale.com/cmd/derper+
        tailscale.com/tailcfg                                        from tailscale.com/client/tailscale+
        tailscale.com/tailfs                                         from tailscale.com/client/tailscale
        tailscale.com/tailfs/compositefs                             from tailscale.com/tailfs
        tailscale.com/tailfs/shared                                  from tailscale.com/tailfs/compositefs+
        tailscale.com/tailfs/webdavfs                                from tailscale.com/tailfs
        tailscale.com/tka                                            from tailscale.com/client/tailscale+
   W    tailscale.com/tsconst                                        from tailscale.com/net/interfaces
        tailscale.com/tstime                                         from tailscale.com/derp+
        tailscale.com/tstime/mono                                    from tailscale.com/tstime/rate
        tailscale.com/tstime/rate                                    from tailscale.com/wgengine/filter+
        tailscale.com/tstime/rate                                    from tailscale.com/derp+
        tailscale.com/tsweb                                          from tailscale.com/cmd/derper
        tailscale.com/tsweb/promvarz                                 from tailscale.com/tsweb
        tailscale.com/tsweb/varz                                     from tailscale.com/tsweb+
        tailscale.com/types/dnstype                                  from tailscale.com/tailcfg
        tailscale.com/types/empty                                    from tailscale.com/ipn
        tailscale.com/types/ipproto                                  from tailscale.com/net/flowtrack+
        tailscale.com/types/key                                      from tailscale.com/cmd/derper+
        tailscale.com/types/key                                      from tailscale.com/client/tailscale+
        tailscale.com/types/lazy                                     from tailscale.com/version+
        tailscale.com/types/logger                                   from tailscale.com/cmd/derper+
        tailscale.com/types/netmap                                   from tailscale.com/ipn


@@ 133,9 142,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        tailscale.com/types/preftype                                 from tailscale.com/ipn
        tailscale.com/types/ptr                                      from tailscale.com/hostinfo+
        tailscale.com/types/structs                                  from tailscale.com/ipn+
        tailscale.com/types/tkatype                                  from tailscale.com/types/key+
        tailscale.com/types/views                                    from tailscale.com/ipn/ipnstate+
        tailscale.com/util/clientmetric                              from tailscale.com/net/tshttpproxy+
        tailscale.com/types/tkatype                                  from tailscale.com/client/tailscale+
        tailscale.com/types/views                                    from tailscale.com/ipn+
        tailscale.com/util/clientmetric                              from tailscale.com/net/netmon+
        tailscale.com/util/cloudenv                                  from tailscale.com/hostinfo+
   W    tailscale.com/util/cmpver                                    from tailscale.com/net/tshttpproxy
        tailscale.com/util/ctxkey                                    from tailscale.com/tsweb+


@@ 144,22 153,22 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        tailscale.com/util/httpm                                     from tailscale.com/client/tailscale
        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
   L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns
        tailscale.com/util/mak                                       from tailscale.com/syncs+
        tailscale.com/util/mak                                       from tailscale.com/net/interfaces+
        tailscale.com/util/multierr                                  from tailscale.com/health+
        tailscale.com/util/nocasemaps                                from tailscale.com/types/ipproto
        tailscale.com/util/set                                       from tailscale.com/health+
        tailscale.com/util/set                                       from tailscale.com/derp+
        tailscale.com/util/singleflight                              from tailscale.com/net/dnscache
        tailscale.com/util/slicesx                                   from tailscale.com/cmd/derper+
        tailscale.com/util/syspolicy                                 from tailscale.com/ipn
        tailscale.com/util/vizerror                                  from tailscale.com/tsweb+
        tailscale.com/util/vizerror                                  from tailscale.com/tailcfg+
   W 💣 tailscale.com/util/winutil                                   from tailscale.com/hostinfo+
        tailscale.com/version                                        from tailscale.com/derp+
        tailscale.com/version/distro                                 from tailscale.com/hostinfo+
        tailscale.com/version/distro                                 from tailscale.com/envknob+
        tailscale.com/wgengine/filter                                from tailscale.com/types/netmap
        golang.org/x/crypto/acme                                     from golang.org/x/crypto/acme/autocert
        golang.org/x/crypto/acme/autocert                            from tailscale.com/cmd/derper
        golang.org/x/crypto/argon2                                   from tailscale.com/tka
        golang.org/x/crypto/blake2b                                  from golang.org/x/crypto/nacl/box+
        golang.org/x/crypto/blake2b                                  from golang.org/x/crypto/argon2+
        golang.org/x/crypto/blake2s                                  from tailscale.com/tka
        golang.org/x/crypto/chacha20                                 from golang.org/x/crypto/chacha20poly1305
        golang.org/x/crypto/chacha20poly1305                         from crypto/tls


@@ 179,10 188,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        golang.org/x/net/proxy                                       from tailscale.com/net/netns
   D    golang.org/x/net/route                                       from net+
        golang.org/x/sync/errgroup                                   from github.com/mdlayher/socket+
        golang.org/x/sys/cpu                                         from golang.org/x/crypto/blake2b+
  LD    golang.org/x/sys/unix                                        from github.com/jsimonetti/rtnetlink/internal/unix+
   W    golang.org/x/sys/windows                                     from golang.org/x/sys/windows/registry+
   W    golang.org/x/sys/windows/registry                            from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
        golang.org/x/sync/singleflight                               from github.com/jellydator/ttlcache/v3
        golang.org/x/sys/cpu                                         from github.com/josharian/native+
  LD    golang.org/x/sys/unix                                        from github.com/google/nftables+
   W    golang.org/x/sys/windows                                     from github.com/dblohm7/wingoes+
   W    golang.org/x/sys/windows/registry                            from github.com/dblohm7/wingoes+
   W    golang.org/x/sys/windows/svc                                 from golang.org/x/sys/windows/svc/mgr+
   W    golang.org/x/sys/windows/svc/mgr                             from tailscale.com/util/winutil
        golang.org/x/text/secure/bidirule                            from golang.org/x/net/idna


@@ 194,10 204,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        bytes                                                        from bufio+
        cmp                                                          from slices+
        compress/flate                                               from compress/gzip+
        compress/gzip                                                from internal/profile+
        compress/gzip                                                from google.golang.org/protobuf/internal/impl+
        container/heap                                               from github.com/jellydator/ttlcache/v3+
        container/list                                               from crypto/tls+
        context                                                      from crypto/tls+
        crypto                                                       from crypto/ecdsa+
        crypto                                                       from crypto/ecdh+
        crypto/aes                                                   from crypto/ecdsa+
        crypto/cipher                                                from crypto/aes+
        crypto/des                                                   from crypto/tls+


@@ 222,14 233,15 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        embed                                                        from crypto/internal/nistec+
        encoding                                                     from encoding/json+
        encoding/asn1                                                from crypto/x509+
        encoding/base32                                              from tailscale.com/tka+
        encoding/base32                                              from github.com/fxamacker/cbor/v2+
        encoding/base64                                              from encoding/json+
        encoding/binary                                              from compress/gzip+
        encoding/hex                                                 from crypto/x509+
        encoding/json                                                from expvar+
        encoding/pem                                                 from crypto/tls+
        encoding/xml                                                 from github.com/tailscale/gowebdav+
        errors                                                       from bufio+
        expvar                                                       from tailscale.com/cmd/derper+
        expvar                                                       from github.com/prometheus/client_golang/prometheus+
        flag                                                         from tailscale.com/cmd/derper+
        fmt                                                          from compress/flate+
        go/token                                                     from google.golang.org/protobuf/internal/strs


@@ 243,12 255,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        io/ioutil                                                    from github.com/mitchellh/go-ps+
        log                                                          from expvar+
        log/internal                                                 from log
        maps                                                         from tailscale.com/types/views+
        maps                                                         from tailscale.com/ipn+
        math                                                         from compress/flate+
        math/big                                                     from crypto/dsa+
        math/bits                                                    from compress/flate+
        math/rand                                                    from github.com/mdlayher/netlink+
        mime                                                         from mime/multipart+
        mime                                                         from github.com/prometheus/common/expfmt+
        mime/multipart                                               from net/http
        mime/quotedprintable                                         from mime/multipart
        net                                                          from crypto/tls+


@@ 260,15 272,15 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
        net/textproto                                                from golang.org/x/net/http/httpguts+
        net/url                                                      from crypto/x509+
        os                                                           from crypto/rand+
        os/exec                                                      from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
        os/exec                                                      from github.com/coreos/go-iptables/iptables+
        os/signal                                                    from tailscale.com/cmd/derper
   W    os/user                                                      from tailscale.com/util/winutil
        path                                                         from golang.org/x/crypto/acme/autocert+
        path                                                         from github.com/prometheus/client_golang/prometheus/internal+
        path/filepath                                                from crypto/x509+
        reflect                                                      from crypto/x509+
        regexp                                                       from internal/profile+
        regexp                                                       from github.com/coreos/go-iptables/iptables+
        regexp/syntax                                                from regexp
        runtime/debug                                                from golang.org/x/crypto/acme+
        runtime/debug                                                from github.com/prometheus/client_golang/prometheus+
        runtime/metrics                                              from github.com/prometheus/client_golang/prometheus+
        runtime/pprof                                                from net/http/pprof
        runtime/trace                                                from net/http/pprof+

M cmd/tailscale/cli/cli.go => cmd/tailscale/cli/cli.go +1 -0
@@ 125,6 125,7 @@ change in the future.
			versionCmd,
			webCmd,
			fileCmd,
			shareCmd,
			bugReportCmd,
			certCmd,
			netlockCmd,

A cmd/tailscale/cli/share.go => cmd/tailscale/cli/share.go +209 -0
@@ 0,0 1,209 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package cli

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"

	"github.com/peterbourgon/ff/v3/ffcli"
	"tailscale.com/tailfs"
)

const (
	shareAddUsage    = "[ALPHA] share add <name> <path>"
	shareRemoveUsage = "[ALPHA] share remove <name>"
	shareListUsage   = "[ALPHA] share list"
)

var shareCmd = &ffcli.Command{
	Name:      "share",
	ShortHelp: "Share a directory with your tailnet",
	ShortUsage: strings.Join([]string{
		shareAddUsage,
		shareRemoveUsage,
		shareListUsage,
	}, "\n  "),
	LongHelp:  buildShareLongHelp(),
	UsageFunc: usageFuncNoDefaultValues,
	Subcommands: []*ffcli.Command{
		{
			Name:      "add",
			Exec:      runShareAdd,
			ShortHelp: "add a share",
			UsageFunc: usageFunc,
		},
		{
			Name:      "remove",
			ShortHelp: "remove a share",
			Exec:      runShareRemove,
			UsageFunc: usageFunc,
		},
		{
			Name:      "list",
			ShortHelp: "list current shares",
			Exec:      runShareList,
			UsageFunc: usageFunc,
		},
	},
	Exec: func(context.Context, []string) error {
		return errors.New("share subcommand required; run 'tailscale share -h' for details")
	},
}

// runShareAdd is the entry point for the "tailscale share add" command.
func runShareAdd(ctx context.Context, args []string) error {
	if len(args) != 2 {
		return fmt.Errorf("usage: tailscale %v", shareAddUsage)
	}

	name, path := args[0], args[1]

	err := localClient.TailfsShareAdd(ctx, &tailfs.Share{
		Name: name,
		Path: path,
	})
	if err == nil {
		fmt.Printf("Added share %q at %q\n", name, path)
	}
	return err
}

// runShareRemove is the entry point for the "tailscale share remove" command.
func runShareRemove(ctx context.Context, args []string) error {
	if len(args) != 1 {
		return fmt.Errorf("usage: tailscale %v", shareRemoveUsage)
	}
	name := args[0]

	err := localClient.TailfsShareRemove(ctx, name)
	if err == nil {
		fmt.Printf("Removed share %q\n", name)
	}
	return err
}

// runShareList is the entry point for the "tailscale share list" command.
func runShareList(ctx context.Context, args []string) error {
	if len(args) != 0 {
		return fmt.Errorf("usage: tailscale %v", shareListUsage)
	}

	sharesMap, err := localClient.TailfsShareList(ctx)
	if err != nil {
		return err
	}
	shares := make([]*tailfs.Share, 0, len(sharesMap))
	for _, share := range sharesMap {
		shares = append(shares, share)
	}

	sort.Slice(shares, func(i, j int) bool {
		return shares[i].Name < shares[j].Name
	})

	longestName := 4 // "name"
	longestPath := 4 // "path"
	longestAs := 2   // "as"
	for _, share := range shares {
		if len(share.Name) > longestName {
			longestName = len(share.Name)
		}
		if len(share.Path) > longestPath {
			longestPath = len(share.Path)
		}
		if len(share.As) > longestAs {
			longestAs = len(share.As)
		}
	}
	formatString := fmt.Sprintf("%%-%ds    %%-%ds    %%s\n", longestName, longestPath)
	fmt.Printf(formatString, "name", "path", "as")
	fmt.Printf(formatString, strings.Repeat("-", longestName), strings.Repeat("-", longestPath), strings.Repeat("-", longestAs))
	for _, share := range shares {
		fmt.Printf(formatString, share.Name, share.Path, share.As)
	}

	return nil
}

func buildShareLongHelp() string {
	longHelpAs := ""
	if tailfs.AllowShareAs() {
		longHelpAs = shareLongHelpAs
	}
	return fmt.Sprintf(shareLongHelpBase, longHelpAs)
}

var shareLongHelpBase = `Tailscale share allows you to share directories with other machines on your tailnet.

Each share is identified by a name and points to a directory at a specific path. For example, to share the path /Users/me/Documents under the name "docs", you would run:

	$ tailscale share add docs /Users/me/Documents

Note that the system forces share names to lowercase to avoid problems with clients that don't support case-sensitive filenames.

Share names may only contain the letters a-z, underscore _, parentheses (), or spaces. Leading and trailing spaces are omitted.

All Tailscale shares have a globally unique path consisting of the tailnet, the machine name and the share name. For example, if the above share was created on the machine "mylaptop" on the tailnet "mydomain.com", the share's path would be:

	/mydomain.com/mylaptop/docs

In order to access this share, other machines on the tailnet can connect to the above path on a WebDAV server running at 100.100.100.100:8080, for example:

	http://100.100.100.100:8080/mydomain.com/mylaptop/docs

Permissions to access shares are controlled via ACLs. For example, to give yourself read/write access and give the group "home" read-only access to the above share, use the below ACL grants:

	{
		"src": ["mylogin@domain.com"],
		"dst": ["mylaptop's ip address"],
		"app": {
			"tailscale.com/cap/tailfs": [{
				"shares": ["docs"],
				"access": "rw"
			}]
		}
	},
	{
		"src": ["group:home"],
		"dst": ["mylaptop"],
		"app": {
			"tailscale.com/cap/tailfs": [{
				"shares": ["docs"],
				"access": "ro"
			}]
		}
	}

To categorically give yourself access to all your shares, you can use the below ACL grant:
	{
		"src": ["autogroup:member"],
		"dst": ["autogroup:self"],
		"app": {
			"tailscale.com/cap/tailfs": [{
				"shares": ["*"],
				"access": "rw"
			}]
		}
	},


Whenever either you or anyone in the group "home" connects to the share, they connect as if they are using your local machine user. They'll be able to read the same files as your user and if they create files, those files will be owned by your user.%s

You can remove shares by name, for example you could remove the above share by running:

	$ tailscale share remove docs

You can get a list of currently published shares by running:

	$ tailscale share list`

var shareLongHelpAs = `

If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run:

	$ sudo -u theuser tailscale share add docs /Users/theuser/Documents`

M cmd/tailscale/depaware.txt => cmd/tailscale/depaware.txt +97 -86
@@ 2,13 2,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep

        filippo.io/edwards25519                                      from github.com/hdevalence/ed25519consensus
        filippo.io/edwards25519/field                                from filippo.io/edwards25519
   W 💣 github.com/alexbrainman/sspi                                 from github.com/alexbrainman/sspi/negotiate+
   W 💣 github.com/alexbrainman/sspi                                 from github.com/alexbrainman/sspi/internal/common+
   W    github.com/alexbrainman/sspi/internal/common                 from github.com/alexbrainman/sspi/negotiate
   W 💣 github.com/alexbrainman/sspi/negotiate                       from tailscale.com/net/tshttpproxy
   L    github.com/coreos/go-iptables/iptables                       from tailscale.com/util/linuxfw
   L    github.com/coreos/go-systemd/v22/dbus                        from tailscale.com/clientupdate
   W 💣 github.com/dblohm7/wingoes                                   from tailscale.com/util/winutil/authenticode+
   W 💣 github.com/dblohm7/wingoes                                   from github.com/dblohm7/wingoes/pe+
   W 💣 github.com/dblohm7/wingoes/pe                                from tailscale.com/util/winutil/authenticode
     💣 github.com/djherbis/times                                    from tailscale.com/tailfs
        github.com/fxamacker/cbor/v2                                 from tailscale.com/tka
   L 💣 github.com/godbus/dbus/v5                                    from github.com/coreos/go-systemd/v22/dbus
        github.com/golang/groupcache/lru                             from tailscale.com/net/dnscache


@@ 18,17 19,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
   L    github.com/google/nftables/expr                              from github.com/google/nftables+
   L    github.com/google/nftables/internal/parseexprfunc            from github.com/google/nftables+
   L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
        github.com/google/uuid                                       from tailscale.com/util/quarantine+
        github.com/google/uuid                                       from tailscale.com/clientupdate+
        github.com/gorilla/csrf                                      from tailscale.com/client/web
        github.com/gorilla/securecookie                              from github.com/gorilla/csrf
        github.com/hdevalence/ed25519consensus                       from tailscale.com/tka+
        github.com/hdevalence/ed25519consensus                       from tailscale.com/clientupdate/distsign+
        github.com/jellydator/ttlcache/v3                            from tailscale.com/tailfs/webdavfs
   L    github.com/josharian/native                                  from github.com/mdlayher/netlink+
   L 💣 github.com/jsimonetti/rtnetlink                              from tailscale.com/net/interfaces+
   L    github.com/jsimonetti/rtnetlink/internal/unix                from github.com/jsimonetti/rtnetlink
        github.com/kballard/go-shellquote                            from tailscale.com/cmd/tailscale/cli
     💣 github.com/mattn/go-colorable                                from tailscale.com/cmd/tailscale/cli
     💣 github.com/mattn/go-isatty                                   from github.com/mattn/go-colorable+
   L 💣 github.com/mdlayher/netlink                                  from github.com/jsimonetti/rtnetlink+
   L 💣 github.com/mdlayher/netlink                                  from github.com/google/nftables+
   L 💣 github.com/mdlayher/netlink/nlenc                            from github.com/jsimonetti/rtnetlink+
   L    github.com/mdlayher/netlink/nltest                           from github.com/google/nftables
   L 💣 github.com/mdlayher/socket                                   from github.com/mdlayher/netlink


@@ 51,18 53,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        github.com/tailscale/goupnp/scpd                             from github.com/tailscale/goupnp
        github.com/tailscale/goupnp/soap                             from github.com/tailscale/goupnp+
        github.com/tailscale/goupnp/ssdp                             from github.com/tailscale/goupnp
        github.com/tailscale/gowebdav                                from tailscale.com/tailfs/webdavfs
   L 💣 github.com/tailscale/netlink                                 from tailscale.com/util/linuxfw
        github.com/tailscale/web-client-prebuilt                     from tailscale.com/client/web
        github.com/tailscale/xnet/webdav                             from tailscale.com/tailfs+
        github.com/tailscale/xnet/webdav/internal/xml                from github.com/tailscale/xnet/webdav
        github.com/tcnksm/go-httpstat                                from tailscale.com/net/netcheck
        github.com/toqueteos/webbrowser                              from tailscale.com/cmd/tailscale/cli
   L 💣 github.com/vishvananda/netlink/nl                            from github.com/tailscale/netlink
   L    github.com/vishvananda/netns                                 from github.com/tailscale/netlink+
        github.com/x448/float16                                      from github.com/fxamacker/cbor/v2
     💣 go4.org/mem                                                  from tailscale.com/derp+
        go4.org/netipx                                               from tailscale.com/wgengine/filter+
     💣 go4.org/mem                                                  from tailscale.com/client/tailscale+
        go4.org/netipx                                               from tailscale.com/net/tsaddr+
   W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg           from tailscale.com/net/interfaces+
        k8s.io/client-go/util/homedir                                from tailscale.com/cmd/tailscale/cli
        nhooyr.io/websocket                                          from tailscale.com/derp/derphttp+
        nhooyr.io/websocket                                          from tailscale.com/control/controlhttp+
        nhooyr.io/websocket/internal/errd                            from nhooyr.io/websocket
        nhooyr.io/websocket/internal/util                            from nhooyr.io/websocket
        nhooyr.io/websocket/internal/xsync                           from nhooyr.io/websocket


@@ 71,11 76,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        software.sslmate.com/src/go-pkcs12                           from tailscale.com/cmd/tailscale/cli
        software.sslmate.com/src/go-pkcs12/internal/rc2              from software.sslmate.com/src/go-pkcs12
        tailscale.com                                                from tailscale.com/version
        tailscale.com/atomicfile                                     from tailscale.com/ipn+
        tailscale.com/client/tailscale                               from tailscale.com/cmd/tailscale/cli+
        tailscale.com/client/tailscale/apitype                       from tailscale.com/cmd/tailscale/cli+
        tailscale.com/atomicfile                                     from tailscale.com/cmd/tailscale/cli+
        tailscale.com/client/tailscale                               from tailscale.com/client/web+
        tailscale.com/client/tailscale/apitype                       from tailscale.com/client/tailscale+
        tailscale.com/client/web                                     from tailscale.com/cmd/tailscale/cli
        tailscale.com/clientupdate                                   from tailscale.com/cmd/tailscale/cli+
        tailscale.com/clientupdate                                   from tailscale.com/client/web+
        tailscale.com/clientupdate/distsign                          from tailscale.com/clientupdate
        tailscale.com/cmd/tailscale/cli                              from tailscale.com/cmd/tailscale
        tailscale.com/control/controlbase                            from tailscale.com/control/controlhttp


@@ 84,54 89,58 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        tailscale.com/derp                                           from tailscale.com/derp/derphttp
        tailscale.com/derp/derphttp                                  from tailscale.com/net/netcheck
        tailscale.com/disco                                          from tailscale.com/derp
        tailscale.com/envknob                                        from tailscale.com/cmd/tailscale/cli+
        tailscale.com/envknob                                        from tailscale.com/client/tailscale+
        tailscale.com/health                                         from tailscale.com/net/tlsdial
        tailscale.com/health/healthmsg                               from tailscale.com/cmd/tailscale/cli
        tailscale.com/hostinfo                                       from tailscale.com/net/interfaces+
        tailscale.com/ipn                                            from tailscale.com/cmd/tailscale/cli+
        tailscale.com/ipn/ipnstate                                   from tailscale.com/cmd/tailscale/cli+
        tailscale.com/licenses                                       from tailscale.com/cmd/tailscale/cli+
        tailscale.com/hostinfo                                       from tailscale.com/client/web+
        tailscale.com/ipn                                            from tailscale.com/client/tailscale+
        tailscale.com/ipn/ipnstate                                   from tailscale.com/client/tailscale+
        tailscale.com/licenses                                       from tailscale.com/client/web+
        tailscale.com/metrics                                        from tailscale.com/derp
        tailscale.com/net/dns/recursive                              from tailscale.com/net/dnsfallback
        tailscale.com/net/dnscache                                   from tailscale.com/derp/derphttp+
        tailscale.com/net/dnscache                                   from tailscale.com/control/controlhttp+
        tailscale.com/net/dnsfallback                                from tailscale.com/control/controlhttp
        tailscale.com/net/flowtrack                                  from tailscale.com/wgengine/filter+
        tailscale.com/net/flowtrack                                  from tailscale.com/net/packet+
     💣 tailscale.com/net/interfaces                                 from tailscale.com/cmd/tailscale/cli+
        tailscale.com/net/netaddr                                    from tailscale.com/ipn+
        tailscale.com/net/netcheck                                   from tailscale.com/cmd/tailscale/cli
        tailscale.com/net/neterror                                   from tailscale.com/net/netcheck+
        tailscale.com/net/netknob                                    from tailscale.com/net/netns
        tailscale.com/net/netmon                                     from tailscale.com/net/sockstats+
        tailscale.com/net/netmon                                     from tailscale.com/cmd/tailscale/cli+
        tailscale.com/net/netns                                      from tailscale.com/derp/derphttp+
        tailscale.com/net/netutil                                    from tailscale.com/client/tailscale+
        tailscale.com/net/packet                                     from tailscale.com/wgengine/filter+
        tailscale.com/net/packet                                     from tailscale.com/wgengine/capture+
        tailscale.com/net/ping                                       from tailscale.com/net/netcheck
        tailscale.com/net/portmapper                                 from tailscale.com/net/netcheck+
        tailscale.com/net/portmapper                                 from tailscale.com/cmd/tailscale/cli+
        tailscale.com/net/sockstats                                  from tailscale.com/control/controlhttp+
        tailscale.com/net/stun                                       from tailscale.com/net/netcheck
   L    tailscale.com/net/tcpinfo                                    from tailscale.com/derp
        tailscale.com/net/tlsdial                                    from tailscale.com/derp/derphttp+
        tailscale.com/net/tsaddr                                     from tailscale.com/net/interfaces+
     💣 tailscale.com/net/tshttpproxy                                from tailscale.com/derp/derphttp+
        tailscale.com/net/tlsdial                                    from tailscale.com/cmd/tailscale/cli+
        tailscale.com/net/tsaddr                                     from tailscale.com/client/web+
     💣 tailscale.com/net/tshttpproxy                                from tailscale.com/clientupdate/distsign+
        tailscale.com/net/wsconn                                     from tailscale.com/control/controlhttp+
        tailscale.com/paths                                          from tailscale.com/cmd/tailscale/cli+
     💣 tailscale.com/safesocket                                     from tailscale.com/cmd/tailscale/cli+
        tailscale.com/syncs                                          from tailscale.com/net/netcheck+
        tailscale.com/tailcfg                                        from tailscale.com/cmd/tailscale/cli+
        tailscale.com/paths                                          from tailscale.com/client/tailscale+
     💣 tailscale.com/safesocket                                     from tailscale.com/client/tailscale+
        tailscale.com/syncs                                          from tailscale.com/cmd/tailscale/cli+
        tailscale.com/tailcfg                                        from tailscale.com/client/tailscale+
        tailscale.com/tailfs                                         from tailscale.com/client/tailscale+
        tailscale.com/tailfs/compositefs                             from tailscale.com/tailfs
        tailscale.com/tailfs/shared                                  from tailscale.com/tailfs/compositefs+
        tailscale.com/tailfs/webdavfs                                from tailscale.com/tailfs
        tailscale.com/tka                                            from tailscale.com/client/tailscale+
   W    tailscale.com/tsconst                                        from tailscale.com/net/interfaces
        tailscale.com/tstime                                         from tailscale.com/control/controlhttp+
        tailscale.com/tstime/mono                                    from tailscale.com/tstime/rate
        tailscale.com/tstime/rate                                    from tailscale.com/wgengine/filter+
        tailscale.com/tstime/rate                                    from tailscale.com/cmd/tailscale/cli+
        tailscale.com/types/dnstype                                  from tailscale.com/tailcfg
        tailscale.com/types/empty                                    from tailscale.com/ipn
        tailscale.com/types/ipproto                                  from tailscale.com/net/flowtrack+
        tailscale.com/types/key                                      from tailscale.com/derp+
        tailscale.com/types/lazy                                     from tailscale.com/version+
        tailscale.com/types/logger                                   from tailscale.com/cmd/tailscale/cli+
        tailscale.com/types/key                                      from tailscale.com/client/tailscale+
        tailscale.com/types/lazy                                     from tailscale.com/util/testenv+
        tailscale.com/types/logger                                   from tailscale.com/client/web+
        tailscale.com/types/netmap                                   from tailscale.com/ipn
        tailscale.com/types/nettype                                  from tailscale.com/net/netcheck+
        tailscale.com/types/opt                                      from tailscale.com/net/netcheck+
        tailscale.com/types/opt                                      from tailscale.com/client/tailscale+
        tailscale.com/types/persist                                  from tailscale.com/ipn
        tailscale.com/types/preftype                                 from tailscale.com/cmd/tailscale/cli+
        tailscale.com/types/ptr                                      from tailscale.com/hostinfo+


@@ 146,29 155,29 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        tailscale.com/util/dnsname                                   from tailscale.com/cmd/tailscale/cli+
        tailscale.com/util/groupmember                               from tailscale.com/client/web
        tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
        tailscale.com/util/lineread                                  from tailscale.com/net/interfaces+
        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
   L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns
        tailscale.com/util/mak                                       from tailscale.com/net/netcheck+
        tailscale.com/util/mak                                       from tailscale.com/cmd/tailscale/cli+
        tailscale.com/util/multierr                                  from tailscale.com/control/controlhttp+
        tailscale.com/util/must                                      from tailscale.com/cmd/tailscale/cli+
        tailscale.com/util/must                                      from tailscale.com/clientupdate/distsign+
        tailscale.com/util/nocasemaps                                from tailscale.com/types/ipproto
        tailscale.com/util/quarantine                                from tailscale.com/cmd/tailscale/cli
        tailscale.com/util/set                                       from tailscale.com/health+
        tailscale.com/util/set                                       from tailscale.com/derp+
        tailscale.com/util/singleflight                              from tailscale.com/net/dnscache+
        tailscale.com/util/slicesx                                   from tailscale.com/net/dnscache+
        tailscale.com/util/slicesx                                   from tailscale.com/net/dns/recursive+
        tailscale.com/util/syspolicy                                 from tailscale.com/ipn
        tailscale.com/util/testenv                                   from tailscale.com/cmd/tailscale/cli
        tailscale.com/util/truncate                                  from tailscale.com/cmd/tailscale/cli
        tailscale.com/util/vizerror                                  from tailscale.com/types/ipproto+
     💣 tailscale.com/util/winutil                                   from tailscale.com/hostinfo+
        tailscale.com/util/vizerror                                  from tailscale.com/tailcfg+
     💣 tailscale.com/util/winutil                                   from tailscale.com/clientupdate+
   W 💣 tailscale.com/util/winutil/authenticode                      from tailscale.com/clientupdate
        tailscale.com/version                                        from tailscale.com/cmd/tailscale/cli+
        tailscale.com/version/distro                                 from tailscale.com/cmd/tailscale/cli+
        tailscale.com/version                                        from tailscale.com/client/web+
        tailscale.com/version/distro                                 from tailscale.com/client/web+
        tailscale.com/wgengine/capture                               from tailscale.com/cmd/tailscale/cli
        tailscale.com/wgengine/filter                                from tailscale.com/types/netmap
        golang.org/x/crypto/argon2                                   from tailscale.com/tka
        golang.org/x/crypto/blake2b                                  from golang.org/x/crypto/nacl/box+
        golang.org/x/crypto/blake2s                                  from tailscale.com/control/controlbase+
        golang.org/x/crypto/blake2b                                  from golang.org/x/crypto/argon2+
        golang.org/x/crypto/blake2s                                  from tailscale.com/clientupdate/distsign+
        golang.org/x/crypto/chacha20                                 from golang.org/x/crypto/chacha20poly1305
        golang.org/x/crypto/chacha20poly1305                         from crypto/tls+
        golang.org/x/crypto/cryptobyte                               from crypto/ecdsa+


@@ 188,18 197,19 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        golang.org/x/net/http2/hpack                                 from net/http
        golang.org/x/net/icmp                                        from tailscale.com/net/ping
        golang.org/x/net/idna                                        from golang.org/x/net/http/httpguts+
        golang.org/x/net/ipv4                                        from golang.org/x/net/icmp+
        golang.org/x/net/ipv6                                        from golang.org/x/net/icmp+
        golang.org/x/net/ipv4                                        from github.com/miekg/dns+
        golang.org/x/net/ipv6                                        from github.com/miekg/dns+
        golang.org/x/net/proxy                                       from tailscale.com/net/netns
   D    golang.org/x/net/route                                       from net+
        golang.org/x/oauth2                                          from golang.org/x/oauth2/clientcredentials
        golang.org/x/oauth2/clientcredentials                        from tailscale.com/cmd/tailscale/cli
        golang.org/x/oauth2/internal                                 from golang.org/x/oauth2+
        golang.org/x/sync/errgroup                                   from tailscale.com/derp+
        golang.org/x/sys/cpu                                         from golang.org/x/crypto/blake2b+
  LD    golang.org/x/sys/unix                                        from tailscale.com/net/netns+
   W    golang.org/x/sys/windows                                     from golang.org/x/sys/windows/registry+
   W    golang.org/x/sys/windows/registry                            from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
        golang.org/x/sync/errgroup                                   from github.com/mdlayher/socket+
        golang.org/x/sync/singleflight                               from github.com/jellydator/ttlcache/v3
        golang.org/x/sys/cpu                                         from github.com/josharian/native+
  LD    golang.org/x/sys/unix                                        from github.com/google/nftables+
   W    golang.org/x/sys/windows                                     from github.com/dblohm7/wingoes+
   W    golang.org/x/sys/windows/registry                            from github.com/dblohm7/wingoes+
   W    golang.org/x/sys/windows/svc                                 from golang.org/x/sys/windows/svc/mgr+
   W    golang.org/x/sys/windows/svc/mgr                             from tailscale.com/util/winutil
        golang.org/x/text/secure/bidirule                            from golang.org/x/net/idna


@@ 209,14 219,15 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        golang.org/x/time/rate                                       from tailscale.com/cmd/tailscale/cli+
        archive/tar                                                  from tailscale.com/clientupdate
        bufio                                                        from compress/flate+
        bytes                                                        from bufio+
        bytes                                                        from archive/tar+
        cmp                                                          from slices+
        compress/flate                                               from compress/gzip+
        compress/gzip                                                from net/http+
        compress/zlib                                                from image/png+
        compress/zlib                                                from debug/pe+
        container/heap                                               from github.com/jellydator/ttlcache/v3+
        container/list                                               from crypto/tls+
        context                                                      from crypto/tls+
        crypto                                                       from crypto/ecdsa+
        crypto                                                       from crypto/ecdh+
        crypto/aes                                                   from crypto/ecdsa+
        crypto/cipher                                                from crypto/aes+
        crypto/des                                                   from crypto/tls+


@@ 234,16 245,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        crypto/sha256                                                from crypto/tls+
        crypto/sha512                                                from crypto/ecdsa+
        crypto/subtle                                                from crypto/aes+
        crypto/tls                                                   from github.com/tcnksm/go-httpstat+
        crypto/tls                                                   from github.com/miekg/dns+
        crypto/x509                                                  from crypto/tls+
        crypto/x509/pkix                                             from crypto/x509+
        database/sql/driver                                          from github.com/google/uuid
   W    debug/dwarf                                                  from debug/pe
   W    debug/pe                                                     from github.com/dblohm7/wingoes/pe
        embed                                                        from tailscale.com/cmd/tailscale/cli+
        encoding                                                     from encoding/json+
        embed                                                        from crypto/internal/nistec+
        encoding                                                     from encoding/gob+
        encoding/asn1                                                from crypto/x509+
        encoding/base32                                              from tailscale.com/tka+
        encoding/base32                                              from github.com/fxamacker/cbor/v2+
        encoding/base64                                              from encoding/json+
        encoding/binary                                              from compress/gzip+
        encoding/gob                                                 from github.com/gorilla/securecookie


@@ 251,64 262,64 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
        encoding/json                                                from expvar+
        encoding/pem                                                 from crypto/tls+
        encoding/xml                                                 from github.com/tailscale/goupnp+
        errors                                                       from bufio+
        errors                                                       from archive/tar+
        expvar                                                       from tailscale.com/derp+
        flag                                                         from github.com/peterbourgon/ff/v3+
        fmt                                                          from compress/flate+
        hash                                                         from crypto+
        fmt                                                          from archive/tar+
        hash                                                         from compress/zlib+
        hash/adler32                                                 from compress/zlib
        hash/crc32                                                   from compress/gzip+
        hash/maphash                                                 from go4.org/mem
        html                                                         from tailscale.com/ipn/ipnstate+
        html                                                         from html/template+
        html/template                                                from github.com/gorilla/csrf
        image                                                        from github.com/skip2/go-qrcode+
        image/color                                                  from github.com/skip2/go-qrcode+
        image/png                                                    from github.com/skip2/go-qrcode
        io                                                           from bufio+
        io/fs                                                        from crypto/x509+
        io                                                           from archive/tar+
        io/fs                                                        from archive/tar+
        io/ioutil                                                    from github.com/godbus/dbus/v5+
        log                                                          from expvar+
        log/internal                                                 from log
        maps                                                         from tailscale.com/types/views+
        math                                                         from compress/flate+
        maps                                                         from tailscale.com/clientupdate+
        math                                                         from archive/tar+
        math/big                                                     from crypto/dsa+
        math/bits                                                    from compress/flate+
        math/rand                                                    from math/big+
        mime                                                         from mime/multipart+
        math/rand                                                    from github.com/mdlayher/netlink+
        mime                                                         from github.com/tailscale/xnet/webdav+
        mime/multipart                                               from net/http
        mime/quotedprintable                                         from mime/multipart
        net                                                          from crypto/tls+
        net/http                                                     from expvar+
        net/http/cgi                                                 from tailscale.com/cmd/tailscale/cli
        net/http/httptrace                                           from github.com/tcnksm/go-httpstat+
        net/http/httputil                                            from tailscale.com/cmd/tailscale/cli+
        net/http/httputil                                            from tailscale.com/client/web+
        net/http/internal                                            from net/http+
        net/netip                                                    from net+
        net/netip                                                    from go4.org/netipx+
        net/textproto                                                from golang.org/x/net/http/httpguts+
        net/url                                                      from crypto/x509+
        os                                                           from crypto/rand+
        os/exec                                                      from github.com/toqueteos/webbrowser+
        os/exec                                                      from github.com/coreos/go-iptables/iptables+
        os/signal                                                    from tailscale.com/cmd/tailscale/cli
        os/user                                                      from tailscale.com/util/groupmember+
        path                                                         from html/template+
        path/filepath                                                from crypto/x509+
        reflect                                                      from crypto/x509+
        regexp                                                       from github.com/tailscale/goupnp/httpu+
        os/user                                                      from archive/tar+
        path                                                         from archive/tar+
        path/filepath                                                from archive/tar+
        reflect                                                      from archive/tar+
        regexp                                                       from github.com/coreos/go-iptables/iptables+
        regexp/syntax                                                from regexp
        runtime/debug                                                from tailscale.com/util/singleflight+
        runtime/debug                                                from golang.org/x/sync/singleflight+
        runtime/trace                                                from testing
        slices                                                       from tailscale.com/cmd/tailscale/cli+
        sort                                                         from compress/flate+
        strconv                                                      from compress/flate+
        strings                                                      from bufio+
        sync                                                         from compress/flate+
        slices                                                       from tailscale.com/client/web+
        sort                                                         from archive/tar+
        strconv                                                      from archive/tar+
        strings                                                      from archive/tar+
        sync                                                         from archive/tar+
        sync/atomic                                                  from context+
        syscall                                                      from crypto/rand+
        syscall                                                      from archive/tar+
        testing                                                      from tailscale.com/util/syspolicy
        text/tabwriter                                               from github.com/peterbourgon/ff/v3/ffcli+
        text/template                                                from html/template
        text/template/parse                                          from html/template+
        time                                                         from compress/gzip+
        time                                                         from archive/tar+
        unicode                                                      from bytes+
        unicode/utf16                                                from encoding/asn1+
        unicode/utf16                                                from crypto/x509+
        unicode/utf8                                                 from bufio+

M cmd/tailscaled/depaware.txt => cmd/tailscaled/depaware.txt +129 -119
@@ 6,7 6,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
   W    github.com/alexbrainman/sspi/internal/common                 from github.com/alexbrainman/sspi/negotiate
   W 💣 github.com/alexbrainman/sspi/negotiate                       from tailscale.com/net/tshttpproxy
  LD    github.com/anmitsu/go-shlex                                  from tailscale.com/tempfork/gliderlabs/ssh
   L    github.com/aws/aws-sdk-go-v2/aws                             from github.com/aws/aws-sdk-go-v2/aws/middleware+
   L    github.com/aws/aws-sdk-go-v2/aws                             from github.com/aws/aws-sdk-go-v2/aws/defaults+
   L    github.com/aws/aws-sdk-go-v2/aws/arn                         from tailscale.com/ipn/store/awsstore
   L    github.com/aws/aws-sdk-go-v2/aws/defaults                    from github.com/aws/aws-sdk-go-v2/service/ssm+
   L    github.com/aws/aws-sdk-go-v2/aws/middleware                  from github.com/aws/aws-sdk-go-v2/aws/retry+


@@ 87,10 87,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
   W    github.com/dblohm7/wingoes/internal                          from github.com/dblohm7/wingoes/com
   W 💣 github.com/dblohm7/wingoes/pe                                from tailscale.com/util/osdiag+
  LW 💣 github.com/digitalocean/go-smbios/smbios                     from tailscale.com/posture
     💣 github.com/djherbis/times                                    from tailscale.com/tailfs
        github.com/fxamacker/cbor/v2                                 from tailscale.com/tka
   W 💣 github.com/go-ole/go-ole                                     from github.com/go-ole/go-ole/oleutil+
   W 💣 github.com/go-ole/go-ole/oleutil                             from tailscale.com/wgengine/winnet
   L 💣 github.com/godbus/dbus/v5                                    from tailscale.com/net/dns+
   L 💣 github.com/godbus/dbus/v5                                    from github.com/coreos/go-systemd/v22/dbus+
        github.com/golang/groupcache/lru                             from tailscale.com/net/dnscache
        github.com/google/btree                                      from gvisor.dev/gvisor/pkg/tcpip/header+
   L    github.com/google/nftables                                   from tailscale.com/util/linuxfw


@@ 102,12 103,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        github.com/google/uuid                                       from tailscale.com/clientupdate
        github.com/gorilla/csrf                                      from tailscale.com/client/web
        github.com/gorilla/securecookie                              from github.com/gorilla/csrf
        github.com/hdevalence/ed25519consensus                       from tailscale.com/tka+
        github.com/hdevalence/ed25519consensus                       from tailscale.com/clientupdate/distsign+
   L 💣 github.com/illarion/gonotify                                 from tailscale.com/net/dns
   L    github.com/insomniacslk/dhcp/dhcpv4                          from tailscale.com/net/tstun
   L    github.com/insomniacslk/dhcp/iana                            from github.com/insomniacslk/dhcp/dhcpv4
   L    github.com/insomniacslk/dhcp/interfaces                      from github.com/insomniacslk/dhcp/dhcpv4
   L    github.com/insomniacslk/dhcp/rfc1035label                    from github.com/insomniacslk/dhcp/dhcpv4
        github.com/jellydator/ttlcache/v3                            from tailscale.com/tailfs/webdavfs
   L    github.com/jmespath/go-jmespath                              from github.com/aws/aws-sdk-go-v2/service/ssm
   L    github.com/josharian/native                                  from github.com/mdlayher/netlink+
   L 💣 github.com/jsimonetti/rtnetlink                              from tailscale.com/net/interfaces+


@@ 115,14 117,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        github.com/klauspost/compress                                from github.com/klauspost/compress/zstd
        github.com/klauspost/compress/fse                            from github.com/klauspost/compress/huff0
        github.com/klauspost/compress/huff0                          from github.com/klauspost/compress/zstd
        github.com/klauspost/compress/internal/cpuinfo               from github.com/klauspost/compress/zstd+
        github.com/klauspost/compress/internal/cpuinfo               from github.com/klauspost/compress/huff0+
        github.com/klauspost/compress/internal/snapref               from github.com/klauspost/compress/zstd
        github.com/klauspost/compress/zstd                           from tailscale.com/smallzstd
        github.com/klauspost/compress/zstd/internal/xxhash           from github.com/klauspost/compress/zstd
        github.com/kortschak/wol                                     from tailscale.com/ipn/ipnlocal
  LD    github.com/kr/fs                                             from github.com/pkg/sftp
   L    github.com/mdlayher/genetlink                                from tailscale.com/net/tstun
   L 💣 github.com/mdlayher/netlink                                  from github.com/jsimonetti/rtnetlink+
   L 💣 github.com/mdlayher/netlink                                  from github.com/google/nftables+
   L 💣 github.com/mdlayher/netlink/nlenc                            from github.com/jsimonetti/rtnetlink+
   L    github.com/mdlayher/netlink/nltest                           from github.com/google/nftables
   L    github.com/mdlayher/sdnotify                                 from tailscale.com/util/systemd


@@ 153,8 155,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        github.com/tailscale/goupnp/scpd                             from github.com/tailscale/goupnp
        github.com/tailscale/goupnp/soap                             from github.com/tailscale/goupnp+
        github.com/tailscale/goupnp/ssdp                             from github.com/tailscale/goupnp
        github.com/tailscale/gowebdav                                from tailscale.com/tailfs/webdavfs
        github.com/tailscale/hujson                                  from tailscale.com/ipn/conffile
   L 💣 github.com/tailscale/netlink                                 from tailscale.com/wgengine/router+
   L 💣 github.com/tailscale/netlink                                 from tailscale.com/net/routetable+
        github.com/tailscale/web-client-prebuilt                     from tailscale.com/client/web
     💣 github.com/tailscale/wireguard-go/conn                       from github.com/tailscale/wireguard-go/device+
   W 💣 github.com/tailscale/wireguard-go/conn/winrio                from github.com/tailscale/wireguard-go/conn


@@ 166,6 169,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        github.com/tailscale/wireguard-go/rwcancel                   from github.com/tailscale/wireguard-go/device+
        github.com/tailscale/wireguard-go/tai64n                     from github.com/tailscale/wireguard-go/device
     💣 github.com/tailscale/wireguard-go/tun                        from github.com/tailscale/wireguard-go/device+
        github.com/tailscale/xnet/webdav                             from tailscale.com/tailfs+
        github.com/tailscale/xnet/webdav/internal/xml                from github.com/tailscale/xnet/webdav
        github.com/tcnksm/go-httpstat                                from tailscale.com/net/netcheck
  LD    github.com/u-root/u-root/pkg/termios                         from tailscale.com/ssh/tailssh
   L    github.com/u-root/uio/rand                                   from github.com/insomniacslk/dhcp/dhcpv4


@@ 173,11 178,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
   L 💣 github.com/vishvananda/netlink/nl                            from github.com/tailscale/netlink
   L    github.com/vishvananda/netns                                 from github.com/tailscale/netlink+
        github.com/x448/float16                                      from github.com/fxamacker/cbor/v2
     💣 go4.org/mem                                                  from tailscale.com/control/controlbase+
        go4.org/netipx                                               from tailscale.com/ipn/ipnlocal+
     💣 go4.org/mem                                                  from tailscale.com/client/tailscale+
        go4.org/netipx                                               from inet.af/wf+
   W 💣 golang.zx2c4.com/wintun                                      from github.com/tailscale/wireguard-go/tun+
   W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg           from tailscale.com/net/dns+
        gvisor.dev/gvisor/pkg/atomicbitops                           from gvisor.dev/gvisor/pkg/tcpip+
   W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg           from tailscale.com/cmd/tailscaled+
        gvisor.dev/gvisor/pkg/atomicbitops                           from gvisor.dev/gvisor/pkg/buffer+
        gvisor.dev/gvisor/pkg/bits                                   from gvisor.dev/gvisor/pkg/buffer
     💣 gvisor.dev/gvisor/pkg/buffer                                 from gvisor.dev/gvisor/pkg/tcpip+
        gvisor.dev/gvisor/pkg/context                                from gvisor.dev/gvisor/pkg/refs


@@ 189,9 194,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
     💣 gvisor.dev/gvisor/pkg/sleep                                  from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
     💣 gvisor.dev/gvisor/pkg/state                                  from gvisor.dev/gvisor/pkg/atomicbitops+
        gvisor.dev/gvisor/pkg/state/wire                             from gvisor.dev/gvisor/pkg/state
     💣 gvisor.dev/gvisor/pkg/sync                                   from gvisor.dev/gvisor/pkg/linewriter+
     💣 gvisor.dev/gvisor/pkg/sync                                   from gvisor.dev/gvisor/pkg/atomicbitops+
     💣 gvisor.dev/gvisor/pkg/sync/locking                           from gvisor.dev/gvisor/pkg/tcpip/stack
        gvisor.dev/gvisor/pkg/tcpip                                  from gvisor.dev/gvisor/pkg/tcpip/header+
        gvisor.dev/gvisor/pkg/tcpip                                  from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
        gvisor.dev/gvisor/pkg/tcpip/adapters/gonet                   from tailscale.com/wgengine/netstack
     💣 gvisor.dev/gvisor/pkg/tcpip/checksum                         from gvisor.dev/gvisor/pkg/buffer+
        gvisor.dev/gvisor/pkg/tcpip/hash/jenkins                     from gvisor.dev/gvisor/pkg/tcpip/stack+


@@ 207,20 212,20 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        gvisor.dev/gvisor/pkg/tcpip/network/ipv6                     from tailscale.com/wgengine/netstack
        gvisor.dev/gvisor/pkg/tcpip/ports                            from gvisor.dev/gvisor/pkg/tcpip/stack+
        gvisor.dev/gvisor/pkg/tcpip/seqnum                           from gvisor.dev/gvisor/pkg/tcpip/header+
     💣 gvisor.dev/gvisor/pkg/tcpip/stack                            from gvisor.dev/gvisor/pkg/tcpip/header/parse+
        gvisor.dev/gvisor/pkg/tcpip/transport                        from gvisor.dev/gvisor/pkg/tcpip/transport/internal/network+
     💣 gvisor.dev/gvisor/pkg/tcpip/stack                            from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
        gvisor.dev/gvisor/pkg/tcpip/transport                        from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
        gvisor.dev/gvisor/pkg/tcpip/transport/icmp                   from tailscale.com/wgengine/netstack
        gvisor.dev/gvisor/pkg/tcpip/transport/internal/network       from gvisor.dev/gvisor/pkg/tcpip/transport/raw+
        gvisor.dev/gvisor/pkg/tcpip/transport/internal/network       from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
        gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop          from gvisor.dev/gvisor/pkg/tcpip/transport/raw
        gvisor.dev/gvisor/pkg/tcpip/transport/packet                 from gvisor.dev/gvisor/pkg/tcpip/transport/raw
        gvisor.dev/gvisor/pkg/tcpip/transport/raw                    from gvisor.dev/gvisor/pkg/tcpip/transport/udp+
        gvisor.dev/gvisor/pkg/tcpip/transport/raw                    from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
     💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp                    from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
        gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack           from gvisor.dev/gvisor/pkg/tcpip/stack
        gvisor.dev/gvisor/pkg/tcpip/transport/udp                    from tailscale.com/net/tstun+
        gvisor.dev/gvisor/pkg/tcpip/transport/udp                    from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
        gvisor.dev/gvisor/pkg/waiter                                 from gvisor.dev/gvisor/pkg/context+
        inet.af/peercred                                             from tailscale.com/ipn/ipnauth
   W 💣 inet.af/wf                                                   from tailscale.com/wf
        nhooyr.io/websocket                                          from tailscale.com/derp/derphttp+
        nhooyr.io/websocket                                          from tailscale.com/control/controlhttp+
        nhooyr.io/websocket/internal/errd                            from nhooyr.io/websocket
        nhooyr.io/websocket/internal/util                            from nhooyr.io/websocket
        nhooyr.io/websocket/internal/xsync                           from nhooyr.io/websocket


@@ 228,119 233,123 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        tailscale.com/appc                                           from tailscale.com/ipn/ipnlocal
        tailscale.com/atomicfile                                     from tailscale.com/ipn+
  LD    tailscale.com/chirp                                          from tailscale.com/cmd/tailscaled
        tailscale.com/client/tailscale                               from tailscale.com/derp+
        tailscale.com/client/tailscale/apitype                       from tailscale.com/ipn/ipnlocal+
        tailscale.com/client/tailscale                               from tailscale.com/client/web+
        tailscale.com/client/tailscale/apitype                       from tailscale.com/client/tailscale+
        tailscale.com/client/web                                     from tailscale.com/ipn/ipnlocal
        tailscale.com/clientupdate                                   from tailscale.com/ipn/ipnlocal+
        tailscale.com/clientupdate                                   from tailscale.com/client/web+
        tailscale.com/clientupdate/distsign                          from tailscale.com/clientupdate
        tailscale.com/cmd/tailscaled/childproc                       from tailscale.com/ssh/tailssh+
        tailscale.com/cmd/tailscaled/childproc                       from tailscale.com/cmd/tailscaled+
        tailscale.com/control/controlbase                            from tailscale.com/control/controlclient+
        tailscale.com/control/controlclient                          from tailscale.com/ipn/ipnlocal+
        tailscale.com/control/controlclient                          from tailscale.com/cmd/tailscaled+
        tailscale.com/control/controlhttp                            from tailscale.com/control/controlclient
        tailscale.com/control/controlknobs                           from tailscale.com/control/controlclient+
        tailscale.com/derp                                           from tailscale.com/derp/derphttp+
        tailscale.com/derp/derphttp                                  from tailscale.com/net/netcheck+
        tailscale.com/derp/derphttp                                  from tailscale.com/cmd/tailscaled+
        tailscale.com/disco                                          from tailscale.com/derp+
        tailscale.com/doctor                                         from tailscale.com/ipn/ipnlocal
     💣 tailscale.com/doctor/permissions                             from tailscale.com/ipn/ipnlocal
        tailscale.com/doctor/routetable                              from tailscale.com/ipn/ipnlocal
        tailscale.com/envknob                                        from tailscale.com/control/controlclient+
        tailscale.com/envknob                                        from tailscale.com/client/tailscale+
        tailscale.com/health                                         from tailscale.com/control/controlclient+
        tailscale.com/health/healthmsg                               from tailscale.com/ipn/ipnlocal
        tailscale.com/hostinfo                                       from tailscale.com/control/controlclient+
        tailscale.com/ipn                                            from tailscale.com/ipn/ipnlocal+
        tailscale.com/hostinfo                                       from tailscale.com/client/web+
        tailscale.com/ipn                                            from tailscale.com/client/tailscale+
        tailscale.com/ipn/conffile                                   from tailscale.com/cmd/tailscaled+
     💣 tailscale.com/ipn/ipnauth                                    from tailscale.com/ipn/ipnlocal+
        tailscale.com/ipn/ipnlocal                                   from tailscale.com/ssh/tailssh+
        tailscale.com/ipn/ipnlocal                                   from tailscale.com/cmd/tailscaled+
        tailscale.com/ipn/ipnserver                                  from tailscale.com/cmd/tailscaled
        tailscale.com/ipn/ipnstate                                   from tailscale.com/control/controlclient+
        tailscale.com/ipn/ipnstate                                   from tailscale.com/client/tailscale+
        tailscale.com/ipn/localapi                                   from tailscale.com/ipn/ipnserver
        tailscale.com/ipn/policy                                     from tailscale.com/ipn/ipnlocal
        tailscale.com/ipn/store                                      from tailscale.com/ipn/ipnlocal+
        tailscale.com/ipn/store                                      from tailscale.com/cmd/tailscaled+
   L    tailscale.com/ipn/store/awsstore                             from tailscale.com/ipn/store
   L    tailscale.com/ipn/store/kubestore                            from tailscale.com/ipn/store
        tailscale.com/ipn/store/mem                                  from tailscale.com/ipn/store+
        tailscale.com/ipn/store/mem                                  from tailscale.com/ipn/ipnlocal+
   L    tailscale.com/kube                                           from tailscale.com/ipn/store/kubestore
        tailscale.com/licenses                                       from tailscale.com/client/web
        tailscale.com/log/filelogger                                 from tailscale.com/logpolicy
        tailscale.com/log/sockstatlog                                from tailscale.com/ipn/ipnlocal
        tailscale.com/logpolicy                                      from tailscale.com/cmd/tailscaled+
        tailscale.com/logtail                                        from tailscale.com/control/controlclient+
        tailscale.com/logtail/backoff                                from tailscale.com/control/controlclient+
        tailscale.com/logtail/filch                                  from tailscale.com/logpolicy+
        tailscale.com/logtail                                        from tailscale.com/cmd/tailscaled+
        tailscale.com/logtail/backoff                                from tailscale.com/cmd/tailscaled+
        tailscale.com/logtail/filch                                  from tailscale.com/log/sockstatlog+
        tailscale.com/metrics                                        from tailscale.com/derp+
        tailscale.com/net/connstats                                  from tailscale.com/net/tstun+
        tailscale.com/net/dns                                        from tailscale.com/ipn/ipnlocal+
        tailscale.com/net/dns/publicdns                              from tailscale.com/net/dns/resolver+
        tailscale.com/net/dns                                        from tailscale.com/cmd/tailscaled+
        tailscale.com/net/dns/publicdns                              from tailscale.com/net/dns+
        tailscale.com/net/dns/recursive                              from tailscale.com/net/dnsfallback
        tailscale.com/net/dns/resolvconffile                         from tailscale.com/net/dns+
        tailscale.com/net/dns/resolver                               from tailscale.com/net/dns
        tailscale.com/net/dnscache                                   from tailscale.com/control/controlclient+
        tailscale.com/net/dnsfallback                                from tailscale.com/control/controlclient+
        tailscale.com/net/dnsfallback                                from tailscale.com/cmd/tailscaled+
        tailscale.com/net/flowtrack                                  from tailscale.com/net/packet+
     💣 tailscale.com/net/interfaces                                 from tailscale.com/control/controlclient+
     💣 tailscale.com/net/interfaces                                 from tailscale.com/cmd/tailscaled+
        tailscale.com/net/netaddr                                    from tailscale.com/ipn+
        tailscale.com/net/netcheck                                   from tailscale.com/wgengine/magicsock
        tailscale.com/net/neterror                                   from tailscale.com/net/dns/resolver+
        tailscale.com/net/netkernelconf                              from tailscale.com/ipn/ipnlocal
        tailscale.com/net/netknob                                    from tailscale.com/net/netns+
        tailscale.com/net/netknob                                    from tailscale.com/logpolicy+
        tailscale.com/net/netmon                                     from tailscale.com/cmd/tailscaled+
        tailscale.com/net/netns                                      from tailscale.com/derp/derphttp+
        tailscale.com/net/netns                                      from tailscale.com/cmd/tailscaled+
   W 💣 tailscale.com/net/netstat                                    from tailscale.com/portlist
        tailscale.com/net/netutil                                    from tailscale.com/ipn/ipnlocal+
        tailscale.com/net/packet                                     from tailscale.com/net/tstun+
        tailscale.com/net/netutil                                    from tailscale.com/client/tailscale+
        tailscale.com/net/packet                                     from tailscale.com/net/connstats+
        tailscale.com/net/packet/checksum                            from tailscale.com/net/tstun
        tailscale.com/net/ping                                       from tailscale.com/net/netcheck+
        tailscale.com/net/portmapper                                 from tailscale.com/net/netcheck+
        tailscale.com/net/portmapper                                 from tailscale.com/ipn/localapi+
        tailscale.com/net/proxymux                                   from tailscale.com/cmd/tailscaled
        tailscale.com/net/routetable                                 from tailscale.com/doctor/routetable
        tailscale.com/net/socks5                                     from tailscale.com/cmd/tailscaled
        tailscale.com/net/sockstats                                  from tailscale.com/control/controlclient+
        tailscale.com/net/stun                                       from tailscale.com/net/netcheck+
        tailscale.com/net/stun                                       from tailscale.com/ipn/localapi+
   L    tailscale.com/net/tcpinfo                                    from tailscale.com/derp
        tailscale.com/net/tlsdial                                    from tailscale.com/control/controlclient+
        tailscale.com/net/tsaddr                                     from tailscale.com/ipn+
        tailscale.com/net/tsdial                                     from tailscale.com/control/controlclient+
     💣 tailscale.com/net/tshttpproxy                                from tailscale.com/control/controlclient+
        tailscale.com/net/tsaddr                                     from tailscale.com/client/web+
        tailscale.com/net/tsdial                                     from tailscale.com/cmd/tailscaled+
     💣 tailscale.com/net/tshttpproxy                                from tailscale.com/clientupdate/distsign+
        tailscale.com/net/tstun                                      from tailscale.com/cmd/tailscaled+
        tailscale.com/net/tstun/table                                from tailscale.com/net/tstun
        tailscale.com/net/wsconn                                     from tailscale.com/control/controlhttp+
        tailscale.com/paths                                          from tailscale.com/ipn/ipnlocal+
        tailscale.com/paths                                          from tailscale.com/client/tailscale+
     💣 tailscale.com/portlist                                       from tailscale.com/ipn/ipnlocal
        tailscale.com/posture                                        from tailscale.com/ipn/ipnlocal
        tailscale.com/proxymap                                       from tailscale.com/tsd+
     💣 tailscale.com/safesocket                                     from tailscale.com/client/tailscale+
        tailscale.com/smallzstd                                      from tailscale.com/control/controlclient+
  LD 💣 tailscale.com/ssh/tailssh                                    from tailscale.com/cmd/tailscaled
        tailscale.com/syncs                                          from tailscale.com/net/netcheck+
        tailscale.com/tailcfg                                        from tailscale.com/client/tailscale/apitype+
        tailscale.com/syncs                                          from tailscale.com/cmd/tailscaled+
        tailscale.com/tailcfg                                        from tailscale.com/client/tailscale+
        tailscale.com/taildrop                                       from tailscale.com/ipn/ipnlocal+
        tailscale.com/tailfs                                         from tailscale.com/client/tailscale+
        tailscale.com/tailfs/compositefs                             from tailscale.com/tailfs
        tailscale.com/tailfs/shared                                  from tailscale.com/tailfs/compositefs+
        tailscale.com/tailfs/webdavfs                                from tailscale.com/tailfs
     💣 tailscale.com/tempfork/device                                from tailscale.com/net/tstun/table
  LD    tailscale.com/tempfork/gliderlabs/ssh                        from tailscale.com/ssh/tailssh
        tailscale.com/tempfork/heap                                  from tailscale.com/wgengine/magicsock
        tailscale.com/tka                                            from tailscale.com/ipn/ipnlocal+
        tailscale.com/tka                                            from tailscale.com/client/tailscale+
   W    tailscale.com/tsconst                                        from tailscale.com/net/interfaces
        tailscale.com/tsd                                            from tailscale.com/cmd/tailscaled+
        tailscale.com/tstime                                         from tailscale.com/wgengine/magicsock+
        tailscale.com/tstime                                         from tailscale.com/control/controlclient+
        tailscale.com/tstime/mono                                    from tailscale.com/net/tstun+
        tailscale.com/tstime/rate                                    from tailscale.com/wgengine/filter+
        tailscale.com/tstime/rate                                    from tailscale.com/derp+
        tailscale.com/tsweb/varz                                     from tailscale.com/cmd/tailscaled
        tailscale.com/types/appctype                                 from tailscale.com/ipn/ipnlocal
        tailscale.com/types/dnstype                                  from tailscale.com/ipn/ipnlocal+
        tailscale.com/types/empty                                    from tailscale.com/ipn+
        tailscale.com/types/flagtype                                 from tailscale.com/cmd/tailscaled
        tailscale.com/types/ipproto                                  from tailscale.com/net/flowtrack+
        tailscale.com/types/key                                      from tailscale.com/control/controlbase+
        tailscale.com/types/lazy                                     from tailscale.com/version+
        tailscale.com/types/logger                                   from tailscale.com/control/controlclient+
        tailscale.com/types/logid                                    from tailscale.com/logtail+
        tailscale.com/types/key                                      from tailscale.com/client/tailscale+
        tailscale.com/types/lazy                                     from tailscale.com/ipn/ipnlocal+
        tailscale.com/types/logger                                   from tailscale.com/appc+
        tailscale.com/types/logid                                    from tailscale.com/cmd/tailscaled+
        tailscale.com/types/netlogtype                               from tailscale.com/net/connstats+
        tailscale.com/types/netmap                                   from tailscale.com/control/controlclient+
        tailscale.com/types/nettype                                  from tailscale.com/wgengine/magicsock+
        tailscale.com/types/nettype                                  from tailscale.com/ipn/localapi+
        tailscale.com/types/opt                                      from tailscale.com/client/tailscale+
        tailscale.com/types/persist                                  from tailscale.com/control/controlclient+
        tailscale.com/types/preftype                                 from tailscale.com/ipn+
        tailscale.com/types/ptr                                      from tailscale.com/hostinfo+
        tailscale.com/types/ptr                                      from tailscale.com/control/controlclient+
        tailscale.com/types/structs                                  from tailscale.com/control/controlclient+
        tailscale.com/types/tkatype                                  from tailscale.com/tka+
        tailscale.com/types/views                                    from tailscale.com/ipn/ipnlocal+


@@ 350,58 359,58 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        tailscale.com/util/ctxkey                                    from tailscale.com/ipn/ipnlocal+
     💣 tailscale.com/util/deephash                                  from tailscale.com/ipn/ipnlocal+
   L 💣 tailscale.com/util/dirwalk                                   from tailscale.com/metrics+
        tailscale.com/util/dnsname                                   from tailscale.com/hostinfo+
        tailscale.com/util/dnsname                                   from tailscale.com/appc+
        tailscale.com/util/execqueue                                 from tailscale.com/control/controlclient+
        tailscale.com/util/goroutines                                from tailscale.com/ipn/ipnlocal
        tailscale.com/util/groupmember                               from tailscale.com/ipn/ipnauth+
        tailscale.com/util/groupmember                               from tailscale.com/client/web+
     💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
        tailscale.com/util/httphdr                                   from tailscale.com/ipn/ipnlocal+
        tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
   L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns+
        tailscale.com/util/mak                                       from tailscale.com/control/controlclient+
        tailscale.com/util/multierr                                  from tailscale.com/control/controlclient+
        tailscale.com/util/must                                      from tailscale.com/logpolicy+
        tailscale.com/util/multierr                                  from tailscale.com/cmd/tailscaled+
        tailscale.com/util/must                                      from tailscale.com/clientupdate/distsign+
        tailscale.com/util/nocasemaps                                from tailscale.com/types/ipproto
     💣 tailscale.com/util/osdiag                                    from tailscale.com/cmd/tailscaled+
   W 💣 tailscale.com/util/osdiag/internal/wsc                       from tailscale.com/util/osdiag
        tailscale.com/util/osshare                                   from tailscale.com/ipn/ipnlocal+
        tailscale.com/util/osuser                                    from tailscale.com/ssh/tailssh+
        tailscale.com/util/osshare                                   from tailscale.com/cmd/tailscaled+
        tailscale.com/util/osuser                                    from tailscale.com/ipn/localapi+
        tailscale.com/util/race                                      from tailscale.com/net/dns/resolver
        tailscale.com/util/racebuild                                 from tailscale.com/logpolicy
        tailscale.com/util/rands                                     from tailscale.com/ipn/ipnlocal+
        tailscale.com/util/ringbuffer                                from tailscale.com/wgengine/magicsock
        tailscale.com/util/set                                       from tailscale.com/health+
        tailscale.com/util/set                                       from tailscale.com/derp+
        tailscale.com/util/singleflight                              from tailscale.com/control/controlclient+
        tailscale.com/util/slicesx                                   from tailscale.com/net/dnscache+
        tailscale.com/util/slicesx                                   from tailscale.com/net/dns/recursive+
        tailscale.com/util/syspolicy                                 from tailscale.com/cmd/tailscaled+
        tailscale.com/util/sysresources                              from tailscale.com/wgengine/magicsock
        tailscale.com/util/systemd                                   from tailscale.com/control/controlclient+
        tailscale.com/util/testenv                                   from tailscale.com/ipn/ipnlocal+
        tailscale.com/util/uniq                                      from tailscale.com/wgengine/magicsock+
        tailscale.com/util/vizerror                                  from tailscale.com/types/ipproto+
        tailscale.com/util/uniq                                      from tailscale.com/ipn/ipnlocal+
        tailscale.com/util/vizerror                                  from tailscale.com/tailcfg+
     💣 tailscale.com/util/winutil                                   from tailscale.com/clientupdate+
   W 💣 tailscale.com/util/winutil/authenticode                      from tailscale.com/util/osdiag+
   W 💣 tailscale.com/util/winutil/authenticode                      from tailscale.com/clientupdate+
   W    tailscale.com/util/winutil/policy                            from tailscale.com/ipn/ipnlocal
        tailscale.com/version                                        from tailscale.com/derp+
        tailscale.com/version/distro                                 from tailscale.com/hostinfo+
        tailscale.com/version                                        from tailscale.com/client/web+
        tailscale.com/version/distro                                 from tailscale.com/client/web+
   W    tailscale.com/wf                                             from tailscale.com/cmd/tailscaled
        tailscale.com/wgengine                                       from tailscale.com/ipn/ipnlocal+
        tailscale.com/wgengine                                       from tailscale.com/cmd/tailscaled+
        tailscale.com/wgengine/capture                               from tailscale.com/ipn/ipnlocal+
        tailscale.com/wgengine/filter                                from tailscale.com/control/controlclient+
     💣 tailscale.com/wgengine/magicsock                             from tailscale.com/ipn/ipnlocal+
        tailscale.com/wgengine/netlog                                from tailscale.com/wgengine
        tailscale.com/wgengine/netstack                              from tailscale.com/cmd/tailscaled
        tailscale.com/wgengine/router                                from tailscale.com/ipn/ipnlocal+
        tailscale.com/wgengine/router                                from tailscale.com/cmd/tailscaled+
        tailscale.com/wgengine/wgcfg                                 from tailscale.com/ipn/ipnlocal+
        tailscale.com/wgengine/wgcfg/nmcfg                           from tailscale.com/ipn/ipnlocal
     💣 tailscale.com/wgengine/wgint                                 from tailscale.com/wgengine
        tailscale.com/wgengine/wglog                                 from tailscale.com/wgengine
   W 💣 tailscale.com/wgengine/winnet                                from tailscale.com/wgengine/router
        golang.org/x/crypto/argon2                                   from tailscale.com/tka
        golang.org/x/crypto/blake2b                                  from golang.org/x/crypto/nacl/box+
        golang.org/x/crypto/blake2b                                  from golang.org/x/crypto/argon2+
        golang.org/x/crypto/blake2s                                  from github.com/tailscale/wireguard-go/device+
  LD    golang.org/x/crypto/blowfish                                 from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf+
  LD    golang.org/x/crypto/blowfish                                 from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf+
        golang.org/x/crypto/chacha20                                 from golang.org/x/crypto/chacha20poly1305+
        golang.org/x/crypto/chacha20poly1305                         from crypto/tls+
        golang.org/x/crypto/cryptobyte                               from crypto/ecdsa+


@@ 412,9 421,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        golang.org/x/crypto/nacl/secretbox                           from golang.org/x/crypto/nacl/box
        golang.org/x/crypto/poly1305                                 from github.com/tailscale/wireguard-go/device+
        golang.org/x/crypto/salsa20/salsa                            from golang.org/x/crypto/nacl/box+
  LD    golang.org/x/crypto/ssh                                      from tailscale.com/ssh/tailssh+
  LD    golang.org/x/crypto/ssh                                      from github.com/pkg/sftp+
        golang.org/x/exp/constraints                                 from github.com/dblohm7/wingoes/pe+
        golang.org/x/exp/maps                                        from tailscale.com/wgengine/magicsock+
        golang.org/x/exp/maps                                        from tailscale.com/appc+
        golang.org/x/net/bpf                                         from github.com/mdlayher/genetlink+
        golang.org/x/net/dns/dnsmessage                              from net+
        golang.org/x/net/http/httpguts                               from golang.org/x/net/http2+


@@ 424,15 433,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        golang.org/x/net/http2/hpack                                 from golang.org/x/net/http2+
        golang.org/x/net/icmp                                        from tailscale.com/net/ping
        golang.org/x/net/idna                                        from golang.org/x/net/http/httpguts+
        golang.org/x/net/ipv4                                        from github.com/tailscale/wireguard-go/conn+
        golang.org/x/net/ipv6                                        from github.com/tailscale/wireguard-go/conn+
        golang.org/x/net/ipv4                                        from github.com/miekg/dns+
        golang.org/x/net/ipv6                                        from github.com/miekg/dns+
        golang.org/x/net/proxy                                       from tailscale.com/net/netns
   D    golang.org/x/net/route                                       from net+
        golang.org/x/sync/errgroup                                   from github.com/mdlayher/socket+
        golang.org/x/sys/cpu                                         from golang.org/x/crypto/blake2b+
  LD    golang.org/x/sys/unix                                        from github.com/insomniacslk/dhcp/interfaces+
   W    golang.org/x/sys/windows                                     from github.com/go-ole/go-ole+
   W    golang.org/x/sys/windows/registry                            from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
        golang.org/x/sync/singleflight                               from github.com/jellydator/ttlcache/v3
        golang.org/x/sys/cpu                                         from github.com/josharian/native+
  LD    golang.org/x/sys/unix                                        from github.com/google/nftables+
   W    golang.org/x/sys/windows                                     from github.com/dblohm7/wingoes+
   W    golang.org/x/sys/windows/registry                            from github.com/dblohm7/wingoes+
   W    golang.org/x/sys/windows/svc                                 from golang.org/x/sys/windows/svc/mgr+
   W    golang.org/x/sys/windows/svc/eventlog                        from tailscale.com/cmd/tailscaled
   W    golang.org/x/sys/windows/svc/mgr                             from tailscale.com/cmd/tailscaled+


@@ 441,18 451,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        golang.org/x/text/transform                                  from golang.org/x/text/secure/bidirule+
        golang.org/x/text/unicode/bidi                               from golang.org/x/net/idna+
        golang.org/x/text/unicode/norm                               from golang.org/x/net/idna
        golang.org/x/time/rate                                       from gvisor.dev/gvisor/pkg/tcpip/stack+
        golang.org/x/time/rate                                       from gvisor.dev/gvisor/pkg/log+
        archive/tar                                                  from tailscale.com/clientupdate
        bufio                                                        from compress/flate+
        bytes                                                        from bufio+
        bytes                                                        from archive/tar+
        cmp                                                          from slices+
        compress/flate                                               from compress/gzip+
        compress/gzip                                                from golang.org/x/net/http2+
   W    compress/zlib                                                from debug/pe
        container/heap                                               from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
        container/heap                                               from github.com/jellydator/ttlcache/v3+
        container/list                                               from crypto/tls+
        context                                                      from crypto/tls+
        crypto                                                       from crypto/ecdsa+
        crypto                                                       from crypto/ecdh+
        crypto/aes                                                   from crypto/ecdsa+
        crypto/cipher                                                from crypto/aes+
        crypto/des                                                   from crypto/tls+


@@ 470,46 480,46 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        crypto/sha256                                                from crypto/tls+
        crypto/sha512                                                from crypto/ecdsa+
        crypto/subtle                                                from crypto/aes+
        crypto/tls                                                   from github.com/tcnksm/go-httpstat+
        crypto/tls                                                   from github.com/aws/aws-sdk-go-v2/aws/transport/http+
        crypto/x509                                                  from crypto/tls+
        crypto/x509/pkix                                             from crypto/x509+
        database/sql/driver                                          from github.com/google/uuid
   W    debug/dwarf                                                  from debug/pe
   W    debug/pe                                                     from github.com/dblohm7/wingoes/pe
        embed                                                        from tailscale.com+
        encoding                                                     from encoding/json+
        embed                                                        from crypto/internal/nistec+
        encoding                                                     from encoding/gob+
        encoding/asn1                                                from crypto/x509+
        encoding/base32                                              from tailscale.com/tka+
        encoding/base32                                              from github.com/fxamacker/cbor/v2+
        encoding/base64                                              from encoding/json+
        encoding/binary                                              from compress/gzip+
        encoding/gob                                                 from github.com/gorilla/securecookie
        encoding/hex                                                 from crypto/x509+
        encoding/json                                                from expvar+
        encoding/pem                                                 from crypto/tls+
        encoding/xml                                                 from github.com/tailscale/goupnp+
        errors                                                       from bufio+
        encoding/xml                                                 from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
        errors                                                       from archive/tar+
        expvar                                                       from tailscale.com/derp+
        flag                                                         from net/http/httptest+
        fmt                                                          from compress/flate+
        hash                                                         from crypto+
        fmt                                                          from archive/tar+
        hash                                                         from compress/zlib+
        hash/adler32                                                 from compress/zlib+
        hash/crc32                                                   from compress/gzip+
        hash/fnv                                                     from tailscale.com/wgengine/magicsock
        hash/maphash                                                 from go4.org/mem
        html                                                         from tailscale.com/ipn/ipnlocal+
        html                                                         from html/template+
        html/template                                                from github.com/gorilla/csrf
        io                                                           from bufio+
        io/fs                                                        from crypto/x509+
        io/ioutil                                                    from github.com/godbus/dbus/v5+
        io                                                           from archive/tar+
        io/fs                                                        from archive/tar+
        io/ioutil                                                    from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
        log                                                          from expvar+
        log/internal                                                 from log
  LD    log/syslog                                                   from tailscale.com/ssh/tailssh
        maps                                                         from tailscale.com/types/views+
        math                                                         from compress/flate+
        maps                                                         from tailscale.com/clientupdate+
        math                                                         from archive/tar+
        math/big                                                     from crypto/dsa+
        math/bits                                                    from compress/flate+
        math/rand                                                    from github.com/mdlayher/netlink+
        mime                                                         from mime/multipart+
        mime                                                         from github.com/tailscale/xnet/webdav+
        mime/multipart                                               from net/http
        mime/quotedprintable                                         from mime/multipart
        net                                                          from crypto/tls+


@@ 520,32 530,32 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
        net/http/internal                                            from net/http+
        net/http/pprof                                               from tailscale.com/cmd/tailscaled+
        net/netip                                                    from github.com/tailscale/wireguard-go/conn+
        net/textproto                                                from golang.org/x/net/http/httpguts+
        net/textproto                                                from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
        net/url                                                      from crypto/x509+
        os                                                           from crypto/rand+
        os/exec                                                      from github.com/coreos/go-iptables/iptables+
        os/exec                                                      from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
        os/signal                                                    from tailscale.com/cmd/tailscaled
        os/user                                                      from github.com/godbus/dbus/v5+
        path                                                         from github.com/godbus/dbus/v5+
        path/filepath                                                from crypto/x509+
        reflect                                                      from crypto/x509+
        regexp                                                       from github.com/coreos/go-iptables/iptables+
        os/user                                                      from archive/tar+
        path                                                         from archive/tar+
        path/filepath                                                from archive/tar+
        reflect                                                      from archive/tar+
        regexp                                                       from github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn+
        regexp/syntax                                                from regexp
        runtime/debug                                                from github.com/klauspost/compress/zstd+
        runtime/pprof                                                from tailscale.com/ipn/ipnlocal+
        runtime/debug                                                from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
        runtime/pprof                                                from net/http/pprof+
        runtime/trace                                                from net/http/pprof+
        slices                                                       from tailscale.com/wgengine/magicsock+
        sort                                                         from compress/flate+
        strconv                                                      from compress/flate+
        strings                                                      from bufio+
        sync                                                         from compress/flate+
        slices                                                       from tailscale.com/appc+
        sort                                                         from archive/tar+
        strconv                                                      from archive/tar+
        strings                                                      from archive/tar+
        sync                                                         from archive/tar+
        sync/atomic                                                  from context+
        syscall                                                      from crypto/rand+
        syscall                                                      from archive/tar+
        testing                                                      from tailscale.com/util/syspolicy
        text/tabwriter                                               from runtime/pprof
        text/template                                                from html/template
        text/template/parse                                          from html/template+
        time                                                         from compress/gzip+
        time                                                         from archive/tar+
        unicode                                                      from bytes+
        unicode/utf16                                                from crypto/x509+
        unicode/utf8                                                 from bufio+

M cmd/tailscaled/tailscaled.go => cmd/tailscaled/tailscaled.go +40 -10
@@ 52,6 52,7 @@ import (
	"tailscale.com/paths"
	"tailscale.com/safesocket"
	"tailscale.com/syncs"
	"tailscale.com/tailfs"
	"tailscale.com/tsd"
	"tailscale.com/tsweb/varz"
	"tailscale.com/types/flagtype"


@@ 135,11 136,12 @@ var (
	createBIRDClient      func(string) (wgengine.BIRDClient, error) // non-nil on some platforms
)

var subCommands = map[string]*func([]string) error{
	"install-system-daemon":   &installSystemDaemon,
	"uninstall-system-daemon": &uninstallSystemDaemon,
	"debug":                   &debugModeFunc,
	"be-child":                &beChildFunc,
var subCommands = map[string]func([]string) error{
	"install-system-daemon":   installSystemDaemon,
	"uninstall-system-daemon": uninstallSystemDaemon,
	"debug":                   debugModeFunc,
	"be-child":                beChild,
	"serve-tailfs":            serveTailfs,
}

var beCLI func() // non-nil if CLI is linked in


@@ 171,12 173,12 @@ func main() {

	if len(os.Args) > 1 {
		sub := os.Args[1]
		if fp, ok := subCommands[sub]; ok {
			if *fp == nil {
		if fn, ok := subCommands[sub]; ok {
			if fn == nil {
				log.SetFlags(0)
				log.Fatalf("%s not available on %v", sub, runtime.GOOS)
			}
			if err := (*fp)(os.Args[2:]); err != nil {
			if err := fn(os.Args[2:]); err != nil {
				log.SetFlags(0)
				log.Fatal(err)
			}


@@ 628,6 630,7 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
		Dialer:       sys.Dialer.Get(),
		SetSubsystem: sys.Set,
		ControlKnobs: sys.ControlKnobs(),
		EnableTailfs: true,
	}

	onlyNetstack = name == "userspace-networking"


@@ 730,6 733,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}

func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
	tfs, _ := sys.TailfsForLocal.GetOK()
	ret, err := netstack.Create(logf,
		sys.Tun.Get(),
		sys.Engine.Get(),


@@ 737,6 741,7 @@ func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
		sys.Dialer.Get(),
		sys.DNSManager.Get(),
		sys.ProxyMapper(),
		tfs,
	)
	if err != nil {
		return nil, err


@@ 792,8 797,6 @@ func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpLis
	return socksListener, httpListener
}

var beChildFunc = beChild

func beChild(args []string) error {
	if len(args) == 0 {
		return errors.New("missing mode argument")


@@ 806,6 809,33 @@ func beChild(args []string) error {
	return f(args[1:])
}

// serveTailfs serves one or more tailfs on localhost using the WebDAV
// protocol. On UNIX and MacOS tailscaled environment, tailfs spawns child
// tailscaled processes in serve-tailfs mode in order to access the fliesystem
// as specific (usually unprivileged) users.
//
// serveTailfs prints the address on which it's listening to stdout so that the
// parent process knows where to connect to.
func serveTailfs(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 := tailfs.NewFileServer()
	if err != nil {
		return fmt.Errorf("unable to start tailfs FileServer: %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
// 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

M cmd/tsconnect/wasm/wasm_js.go => cmd/tsconnect/wasm/wasm_js.go +1 -1
@@ 110,7 110,7 @@ func newIPN(jsConfig js.Value) map[string]any {
	}
	sys.Set(eng)

	ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
	ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
	if err != nil {
		log.Fatalf("netstack.Create: %v", err)
	}

M flake.nix => flake.nix +1 -1
@@ 120,4 120,4 @@
  in
    flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-OfwliH0EmrGoOVog8b27F0p5+foElOrRZkOtGR4+Sts=
# nix-direnv cache busting line: sha256-eci4f6golU1eIQOezplA+I+gmOfof40ktIdpr0v/uMc=

M go.mod => go.mod +4 -0
@@ 22,6 22,7 @@ require (
	github.com/dave/patsy v0.0.0-20210517141501-957256f50cba
	github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
	github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
	github.com/djherbis/times v1.6.0
	github.com/dsnet/try v0.0.3
	github.com/evanw/esbuild v0.19.11
	github.com/frankban/quicktest v1.14.6


@@ 41,6 42,7 @@ require (
	github.com/iancoleman/strcase v0.3.0
	github.com/illarion/gonotify v1.0.1
	github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
	github.com/jellydator/ttlcache/v3 v3.1.0
	github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
	github.com/jsimonetti/rtnetlink v1.4.0
	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51


@@ 65,11 67,13 @@ require (
	github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
	github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780
	github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
	github.com/tailscale/gowebdav v0.0.0-20240130173557-d49b872b5126
	github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
	github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734
	github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
	github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61
	github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272
	github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9
	github.com/tc-hib/winres v0.2.1
	github.com/tcnksm/go-httpstat v0.2.0
	github.com/toqueteos/webbrowser v1.2.0

M go.mod.sri => go.mod.sri +1 -1
@@ 1,1 1,1 @@
sha256-OfwliH0EmrGoOVog8b27F0p5+foElOrRZkOtGR4+Sts=
sha256-eci4f6golU1eIQOezplA+I+gmOfof40ktIdpr0v/uMc=

M go.sum => go.sum +9 -0
@@ 244,6 244,8 @@ github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpFowZBX6GoQ=
github.com/docker/cli v25.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=


@@ 530,6 532,8 @@ github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM=
github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=


@@ 863,6 867,8 @@ github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2C
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/gowebdav v0.0.0-20240130173557-d49b872b5126 h1:EBLH+PeC3efXmUi82yEMxjlcKhDwAUZTi0tIT4Q8oTg=
github.com/tailscale/gowebdav v0.0.0-20240130173557-d49b872b5126/go.mod h1:UCbnLJ2ebWLs28V9ubpXbq4Qx3e0q1TVoM1AC3Z2b40=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734 h1:93cvKHbvsPK3MKfFTvR00d0b0R0bzRKBW9yrj813fhI=


@@ 873,6 879,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 h1:G
github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4CaamMdC3tFoTpzrsUSMDPV0K6rhnQdF7kXekQ=
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=


@@ 1145,6 1153,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

M ipn/backend.go => ipn/backend.go +8 -1
@@ 65,7 65,8 @@ const (
	NotifyInitialPrefs  // if set, the first Notify message (sent immediately) will contain the current Prefs
	NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap

	NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
	NotifyNoPrivateKeys       // if set, private keys that would normally be sent in updates are zeroed out
	NotifyInitialTailfsShares // if set, the first Notify message (sent immediately) will contain the current Tailfs Shares
)

// Notify is a communication from a backend (e.g. tailscaled) to a frontend


@@ 121,6 122,12 @@ type Notify struct {
	// is available.
	ClientVersion *tailcfg.ClientVersion `json:",omitempty"`

	// Full set of current TailfsShares that we're publishing as name->path.
	// Some client applications, like the MacOS and Windows clients, will
	// listen for updates to this and handle serving these shares under the
	// identity of the unprivileged user that is running the application.
	TailfsShares map[string]string `json:",omitempty"`

	// type is mirrored in xcode/Shared/IPN.swift
}


M ipn/ipnlocal/local.go => ipn/ipnlocal/local.go +68 -1
@@ 67,6 67,7 @@ import (
	"tailscale.com/syncs"
	"tailscale.com/tailcfg"
	"tailscale.com/taildrop"
	"tailscale.com/tailfs"
	"tailscale.com/tka"
	"tailscale.com/tsd"
	"tailscale.com/tstime"


@@ 287,6 288,9 @@ type LocalBackend struct {
	serveListeners     map[netip.AddrPort]*localListener // listeners for local serve traffic
	serveProxyHandlers sync.Map                          // string (HTTPHandler.Proxy) => *reverseProxy

	tailfsListeners map[netip.AddrPort]*localListener // listeners for local tailfs traffic
	tailfsForRemote *tailfs.FileSystemForRemote

	// statusLock must be held before calling statusChanged.Wait() or
	// statusChanged.Broadcast().
	statusLock    sync.Mutex


@@ 428,6 432,15 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
		}
	}

	// initialize Tailfs shares from saved state
	b.mu.Lock()
	b.tailfsForRemote = tailfs.NewFileSystemForRemote(logf)
	shares, err := b.tailfsGetSharesLocked()
	b.mu.Unlock()
	if err == nil && len(shares) > 0 {
		b.tailfsForRemote.SetShares(shares)
	}

	return b, nil
}



@@ 915,6 928,7 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.
	var zero tailcfg.NodeView
	b.mu.Lock()
	defer b.mu.Unlock()

	nid, ok := b.nodeByAddr[ipp.Addr()]
	if !ok {
		var ip netip.Addr


@@ 2254,7 2268,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
	b.mu.Lock()
	b.activeWatchSessions.Add(sessionID)

	const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
	const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialTailfsShares
	if mask&initialBits != 0 {
		ini = &ipn.Notify{Version: version.Long()}
		if mask&ipn.NotifyInitialState != 0 {


@@ 2270,6 2284,17 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
		if mask&ipn.NotifyInitialNetMap != 0 {
			ini.NetMap = b.netMap
		}
		if mask&ipn.NotifyInitialTailfsShares != 0 && b.tailfsSharingEnabledLocked() {
			shares, err := b.tailfsGetSharesLocked()
			if err != nil {
				b.logf("unable to notify initial tailfs shares: %v", err)
			} else {
				ini.TailfsShares = make(map[string]string, len(shares))
				for _, share := range shares {
					ini.TailfsShares[share.Name] = share.Path
				}
			}
		}
	}

	handle := b.notifyWatchers.Add(&watchSession{ch, sessionID})


@@ 3312,6 3337,14 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
	if dst.Port() == webClientPort && b.ShouldRunWebClient() {
		return b.handleWebClientConn, opts
	}
	if dst.Port() == TailfsLocalPort {
		fs, ok := b.sys.TailfsForLocal.GetOK()
		if ok {
			return func(conn net.Conn) error {
				return fs.HandleConn(conn, conn.RemoteAddr())
			}, opts
		}
	}
	if port, ok := b.GetPeerAPIPort(dst.Addr()); ok && dst.Port() == port {
		return func(c net.Conn) error {
			b.handlePeerAPIConn(src, dst, c)


@@ 4608,6 4641,11 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
			delete(b.nodeByAddr, k)
		}
	}

	if b.tailfsSharingEnabledLocked() {
		b.updateTailfsPeersLocked(nm)
		b.tailfsNotifyCurrentSharesLocked()
	}
}

func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {


@@ 4615,14 4653,17 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
		b.peers = nil
		return
	}

	// First pass, mark everything unwanted.
	for k := range b.peers {
		b.peers[k] = tailcfg.NodeView{}
	}

	// Second pass, add everything wanted.
	for _, p := range nm.Peers {
		mak.Set(&b.peers, p.ID(), p)
	}

	// Third pass, remove deleted things.
	for k, v := range b.peers {
		if !v.Valid() {


@@ 4631,6 4672,28 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
	}
}

// tailfsTransport is an http.RoundTripper that uses the latest value of
// b.Dialer().PeerAPITransport() for each round trip and imposes a short
// dial timeout to avoid hanging on connecting to offline/unreachable hosts.
type tailfsTransport struct {
	b *LocalBackend
}

func (t *tailfsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	// dialTimeout is fairly aggressive to avoid hangs on contacting offline or
	// unreachable hosts.
	dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this

	tr := t.b.Dialer().PeerAPITransport().Clone()
	dialContext := tr.DialContext
	tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
		ctxWithTimeout, cancel := context.WithTimeout(ctx, dialTimeout)
		defer cancel()
		return dialContext(ctxWithTimeout, network, addr)
	}
	return tr.RoundTrip(req)
}

// setDebugLogsByCapabilityLocked sets debug logging based on the self node's
// capabilities in the provided NetMap.
func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {


@@ 4703,6 4766,10 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
		}
	}

	if !b.sys.IsNetstack() {
		b.updateTailfsListenersLocked()
	}

	b.reloadServeConfigLocked(prefs)
	if b.serveConfig.Valid() {
		servePorts := make([]uint16, 0, 3)

M ipn/ipnlocal/local_test.go => ipn/ipnlocal/local_test.go +1 -1
@@ 803,7 803,7 @@ func TestWatchNotificationsCallbacks(t *testing.T) {

// tests LocalBackend.updateNetmapDeltaLocked
func TestUpdateNetmapDelta(t *testing.T) {
	var b LocalBackend
	b := newTestLocalBackend(t)
	if b.updateNetmapDeltaLocked(nil) {
		t.Errorf("updateNetmapDeltaLocked() = true, want false with nil netmap")
	}

M ipn/ipnlocal/peerapi.go => ipn/ipnlocal/peerapi.go +49 -1
@@ 38,12 38,17 @@ import (
	"tailscale.com/net/sockstats"
	"tailscale.com/tailcfg"
	"tailscale.com/taildrop"
	"tailscale.com/tailfs"
	"tailscale.com/types/views"
	"tailscale.com/util/clientmetric"
	"tailscale.com/util/httphdr"
	"tailscale.com/wgengine/filter"
)

const (
	tailfsPrefix = "/v0/tailfs"
)

var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, string) error

// addH2C is non-nil on platforms where we want to add H2C


@@ 317,6 322,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		h.handleDNSQuery(w, r)
		return
	}
	if strings.HasPrefix(r.URL.Path, tailfsPrefix) {
		h.handleServeTailfs(w, r)
		return
	}
	switch r.URL.Path {
	case "/v0/goroutines":
		h.handleServeGoroutines(w, r)


@@ 626,7 635,11 @@ func (h *peerAPIHandler) canIngress() bool {
}

func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
	return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
	return h.peerCaps().HasCapability(wantCap)
}

func (h *peerAPIHandler) peerCaps() tailcfg.PeerCapMap {
	return h.ps.b.PeerCaps(h.remoteAddr.Addr())
}

func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {


@@ 1090,6 1103,41 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
	return nil
}

func (h *peerAPIHandler) handleServeTailfs(w http.ResponseWriter, r *http.Request) {
	if !h.ps.b.TailfsSharingEnabled() {
		http.Error(w, "tailfs not enabled", http.StatusNotFound)
		return
	}

	capsMap := h.peerCaps()
	tailfsCaps, ok := capsMap[tailcfg.PeerCapabilityTailfs]
	if !ok {
		http.Error(w, "tailfs not permitted", http.StatusForbidden)
		return
	}

	rawPerms := make([][]byte, 0, len(tailfsCaps))
	for _, cap := range tailfsCaps {
		rawPerms = append(rawPerms, []byte(cap))
	}

	p, err := tailfs.ParsePermissions(rawPerms)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	h.ps.b.mu.Lock()
	fs := h.ps.b.tailfsForRemote
	h.ps.b.mu.Unlock()
	if fs == nil {
		http.Error(w, "tailfs not enabled", http.StatusNotFound)
		return
	}
	r.URL.Path = strings.TrimPrefix(r.URL.Path, tailfsPrefix)
	fs.ServeHTTPWithPerms(p, w, r)
}

// newFakePeerAPIListener creates a new net.Listener that acts like
// it's listening on the provided IP address and on TCP port 1.
//

M ipn/ipnlocal/serve.go => ipn/ipnlocal/serve.go +1 -1
@@ 62,7 62,7 @@ type serveHTTPContext struct {
//
// This is not used in userspace-networking mode.
//
// localListener is used by tailscale serve (TCP only) as well as the built-in web client.
// localListener is used by tailscale serve (TCP only), the built-in web client and tailfs.
// Most serve traffic and peer traffic for the web client are intercepted by netstack.
// This listener exists purely for connections from the machine itself, as that goes via the kernel,
// so we need to be in the kernel's listening/routing tables.

A ipn/ipnlocal/tailfs.go => ipn/ipnlocal/tailfs.go +318 -0
@@ 0,0 1,318 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package ipnlocal

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/netip"
	"os"
	"regexp"
	"strings"
	"time"

	"tailscale.com/ipn"
	"tailscale.com/logtail/backoff"
	"tailscale.com/tailcfg"
	"tailscale.com/tailfs"
	"tailscale.com/types/logger"
	"tailscale.com/types/netmap"
)

const (
	// TailfsLocalPort is the port on which the Tailfs listens for location
	// connections on quad 100.
	TailfsLocalPort = 8080

	tailfsSharesStateKey = ipn.StateKey("_tailfs-shares")
)

var (
	shareNameRegex      = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
	errInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
)

// TailfsSharingEnabled reports whether sharing to remote nodes via tailfs is
// enabled. This is currently based on checking for the tailfs:share node
// attribute.
func (b *LocalBackend) TailfsSharingEnabled() bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.tailfsSharingEnabledLocked()
}

func (b *LocalBackend) tailfsSharingEnabledLocked() bool {
	return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailfsSharingEnabled)
}

// TailfsSetFileServerAddr tells tailfs to use the given address for connecting
// to the tailfs.FileServer that's exposing local files as an unprivileged
// user.
func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error {
	b.mu.Lock()
	fs := b.tailfsForRemote
	b.mu.Unlock()
	if fs == nil {
		return errors.New("tailfs not enabled")
	}

	fs.SetFileServerAddr(addr)
	return nil
}

// TailfsAddShare adds the given share if no share with that name exists, or
// replaces the existing share if one with the same name already exists.
// To avoid potential incompatibilities across file systems, share names are
// limited to alphanumeric characters and the underscore _.
func (b *LocalBackend) TailfsAddShare(share *tailfs.Share) error {
	var err error
	share.Name, err = normalizeShareName(share.Name)
	if err != nil {
		return err
	}

	b.mu.Lock()
	shares, err := b.tailfsAddShareLocked(share)
	b.mu.Unlock()
	if err != nil {
		return err
	}

	b.tailfsNotifyShares(shares)
	return nil
}

// normalizeShareName normalizes the given share name and returns an error if
// it contains any disallowed characters.
func normalizeShareName(name string) (string, error) {
	// Force all share names to lowercase to avoid potential incompatibilities
	// with clients that don't support case-sensitive filenames.
	name = strings.ToLower(name)

	// Trim whitespace
	name = strings.TrimSpace(name)

	if !shareNameRegex.MatchString(name) {
		return "", errInvalidShareName
	}

	return name, nil
}

func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]string, error) {
	if b.tailfsForRemote == nil {
		return nil, errors.New("tailfs not enabled")
	}

	shares, err := b.tailfsGetSharesLocked()
	if err != nil {
		return nil, err
	}
	shares[share.Name] = share
	data, err := json.Marshal(shares)
	if err != nil {
		return nil, fmt.Errorf("marshal: %w", err)
	}
	err = b.store.WriteState(tailfsSharesStateKey, data)
	if err != nil {
		return nil, fmt.Errorf("write state: %w", err)
	}
	b.tailfsForRemote.SetShares(shares)

	return shareNameMap(shares), nil
}

// TailfsRemoveShare removes the named share. Share names are forced to
// lowercase.
func (b *LocalBackend) TailfsRemoveShare(name string) error {
	// Force all share names to lowercase to avoid potential incompatibilities
	// with clients that don't support case-sensitive filenames.
	name = strings.ToLower(name)

	b.mu.Lock()
	shares, err := b.tailfsRemoveShareLocked(name)
	b.mu.Unlock()
	if err != nil {
		return err
	}

	b.tailfsNotifyShares(shares)
	return nil
}

func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string, error) {
	if b.tailfsForRemote == nil {
		return nil, errors.New("tailfs not enabled")
	}

	shares, err := b.tailfsGetSharesLocked()
	if err != nil {
		return nil, err
	}
	_, shareExists := shares[name]
	if !shareExists {
		return nil, os.ErrNotExist
	}
	delete(shares, name)
	data, err := json.Marshal(shares)
	if err != nil {
		return nil, fmt.Errorf("marshal: %w", err)
	}
	err = b.store.WriteState(tailfsSharesStateKey, data)
	if err != nil {
		return nil, fmt.Errorf("write state: %w", err)
	}
	b.tailfsForRemote.SetShares(shares)

	return shareNameMap(shares), nil
}

func shareNameMap(sharesByName map[string]*tailfs.Share) map[string]string {
	sharesMap := make(map[string]string, len(sharesByName))
	for _, share := range sharesByName {
		sharesMap[share.Name] = share.Path
	}
	return sharesMap
}

// tailfsNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
// about the latest set of shares, supplied as a map of name -> directory.
func (b *LocalBackend) tailfsNotifyShares(shares map[string]string) {
	b.send(ipn.Notify{TailfsShares: shares})
}

// tailfsNotifyCurrentSharesLocked sends an ipn.Notify with the current set of
// tailfs shares.
func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() {
	shares, err := b.tailfsGetSharesLocked()
	if err != nil {
		b.logf("error notifying current tailfs shares: %v", err)
		return
	}
	// Do the below on a goroutine to avoid deadlocking on b.mu in b.send().
	go b.tailfsNotifyShares(shareNameMap(shares))
}

// TailfsGetShares() returns the current set of shares from the state store.
func (b *LocalBackend) TailfsGetShares() (map[string]*tailfs.Share, error) {
	b.mu.Lock()
	defer b.mu.Unlock()

	return b.tailfsGetSharesLocked()
}

func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error) {
	data, err := b.store.ReadState(tailfsSharesStateKey)
	if err != nil {
		if errors.Is(err, ipn.ErrStateNotExist) {
			return make(map[string]*tailfs.Share), nil
		}
		return nil, fmt.Errorf("read state: %w", err)
	}

	var shares map[string]*tailfs.Share
	err = json.Unmarshal(data, &shares)
	if err != nil {
		return nil, fmt.Errorf("unmarshal: %w", err)
	}

	return shares, nil
}

// updateTailfsListenersLocked creates listeners on the local Tailfs port.
// This is needed to properly route local traffic when using kernel networking
// mode.
func (b *LocalBackend) updateTailfsListenersLocked() {
	if b.netMap == nil {
		return
	}

	addrs := b.netMap.GetAddresses()
	oldListeners := b.tailfsListeners
	newListeners := make(map[netip.AddrPort]*localListener, addrs.Len())
	for i := range addrs.LenIter() {
		if fs, ok := b.sys.TailfsForLocal.GetOK(); ok {
			addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), TailfsLocalPort)
			if sl, ok := b.tailfsListeners[addrPort]; ok {
				newListeners[addrPort] = sl
				delete(oldListeners, addrPort)
				continue // already listening
			}

			sl := b.newTailfsListener(context.Background(), fs, addrPort, b.logf)
			newListeners[addrPort] = sl
			go sl.Run()
		}
	}

	// At this point, anything left in oldListeners can be stopped.
	for _, sl := range oldListeners {
		sl.cancel()
	}
}

// newTailfsListener returns a listener for local connections to a tailfs
// WebDAV FileSystem.
func (b *LocalBackend) newTailfsListener(ctx context.Context, fs *tailfs.FileSystemForLocal, ap netip.AddrPort, logf logger.Logf) *localListener {
	ctx, cancel := context.WithCancel(ctx)
	return &localListener{
		b:      b,
		ap:     ap,
		ctx:    ctx,
		cancel: cancel,
		logf:   logf,

		handler: func(conn net.Conn) error {
			return fs.HandleConn(conn, conn.RemoteAddr())
		},
		bo: backoff.NewBackoff(fmt.Sprintf("tailfs-listener-%d", ap.Port()), logf, 30*time.Second),
	}
}

// updateTailfsPeersLocked sets all applicable peers from the netmap as tailfs
// remotes.
func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
	fs, ok := b.sys.TailfsForLocal.GetOK()
	if !ok {
		return
	}

	tailfsRemotes := make([]*tailfs.Remote, 0, len(nm.Peers))
	for _, p := range nm.Peers {
		peerID := p.ID()
		url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailfsPrefix[1:])
		tailfsRemotes = append(tailfsRemotes, &tailfs.Remote{
			Name: p.DisplayName(false),
			URL:  url,
			Available: func() bool {
				// TODO(oxtoacart): need to figure out a performant and reliable way to only
				// show the peers that have shares to which we have access
				// This will require work on the control server to transmit the inverse
				// of the "tailscale.com/cap/tailfs" capability.
				// For now, at least limit it only to nodes that are online.
				// Note, we have to iterate the latest netmap because the peer we got from the first iteration may not be it
				b.mu.Lock()
				latestNetMap := b.netMap
				b.mu.Unlock()

				for _, candidate := range latestNetMap.Peers {
					if candidate.ID() == peerID {
						online := candidate.Online()
						// TODO(oxtoacart): for some reason, this correctly
						// catches when a node goes from offline to online,
						// but not the other way around...
						return online != nil && *online
					}
				}

				// peer not found, must not be available
				return false
			},
		})
	}
	fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailfsTransport{b: b})
}

A ipn/ipnlocal/tailfs_test.go => ipn/ipnlocal/tailfs_test.go +40 -0
@@ 0,0 1,40 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package ipnlocal

import (
	"fmt"
	"testing"
)

func TestNormalizeShareName(t *testing.T) {
	tests := []struct {
		name string
		want string
		err  error
	}{
		{
			name: "  (_this is A 5 nAme )_ ",
			want: "(_this is a 5 name )_",
		},
		{
			name: "",
			err:  errInvalidShareName,
		},
		{
			name: "generally good except for .",
			err:  errInvalidShareName,
		},
	}
	for _, tt := range tests {
		t.Run(fmt.Sprintf("name %q", tt.name), func(t *testing.T) {
			got, err := normalizeShareName(tt.name)
			if tt.err != nil && err != tt.err {
				t.Errorf("wanted error %v, got %v", tt.err, err)
			} else if got != tt.want {
				t.Errorf("wanted %q, got %q", tt.want, got)
			}
		})
	}
}

M ipn/localapi/localapi.go => ipn/localapi/localapi.go +123 -1
@@ 18,7 18,9 @@ import (
	"net/http/httputil"
	"net/netip"
	"net/url"
	"os"
	"os/exec"
	"path"
	"runtime"
	"slices"
	"strconv"


@@ 41,6 43,7 @@ import (
	"tailscale.com/net/portmapper"
	"tailscale.com/tailcfg"
	"tailscale.com/taildrop"
	"tailscale.com/tailfs"
	"tailscale.com/tka"
	"tailscale.com/tstime"
	"tailscale.com/types/key"


@@ 107,6 110,8 @@ var handler = map[string]localAPIHandler{
	"serve-config":                (*Handler).serveServeConfig,
	"set-dns":                     (*Handler).serveSetDNS,
	"set-expiry-sooner":           (*Handler).serveSetExpirySooner,
	"tailfs/fileserver-address":   (*Handler).serveTailfsFileServerAddr,
	"tailfs/shares":               (*Handler).serveShares,
	"start":                       (*Handler).serveStart,
	"status":                      (*Handler).serveStatus,
	"tka/init":                    (*Handler).serveTKAInit,


@@ 1107,7 1112,7 @@ func (h *Handler) connIsLocalAdmin() bool {
		if err != nil {
			return false
		}
		// Short timeout just in case sudo hands for some reason.
		// Short timeout just in case sudo hangs for some reason.
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {


@@ 1120,6 1125,34 @@ func (h *Handler) connIsLocalAdmin() bool {
	}
}

func (h *Handler) getUsername() (string, error) {
	if h.ConnIdentity == nil {
		h.logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
		return "", errors.New("missing ConnIdentity")
	}
	switch runtime.GOOS {
	case "windows":
		tok, err := h.ConnIdentity.WindowsToken()
		if err != nil {
			return "", fmt.Errorf("get windows token: %w", err)
		}
		defer tok.Close()
		return tok.Username()
	case "darwin", "linux":
		uid, ok := h.ConnIdentity.Creds().UserID()
		if !ok {
			return "", errors.New("missing user ID")
		}
		u, err := osuser.LookupByUID(uid)
		if err != nil {
			return "", fmt.Errorf("lookup user: %w", err)
		}
		return u.Username, nil
	default:
		return "", errors.New("unsupported OS")
	}
}

func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
	if !h.PermitRead {
		http.Error(w, "IP forwarding check access denied", http.StatusForbidden)


@@ 2498,6 2531,95 @@ func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(ups)
}

// serveTailfsFileServerAddr handles updates of the tailfs file server address.
func (h *Handler) serveTailfsFileServerAddr(w http.ResponseWriter, r *http.Request) {
	if r.Method != "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.TailfsSetFileServerAddr(string(b))
	w.WriteHeader(http.StatusCreated)
}

// serveShares handles the management of tailfs shares.
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
	if !h.b.TailfsSharingEnabled() {
		http.Error(w, `tailfs sharing not enabled, please add the attribute "tailfs:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusInternalServerError)
		return
	}
	switch r.Method {
	case "PUT":
		var share tailfs.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 tailfs.AllowShareAs() {
			// share as the connected user
			username, err := h.getUsername()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			share.As = username
		}
		err = h.b.TailfsAddShare(&share)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusCreated)
	case "DELETE":
		var share tailfs.Share
		err := json.NewDecoder(r.Body).Decode(&share)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		err = h.b.TailfsRemoveShare(share.Name)
		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 "GET":
		shares, err := h.b.TailfsGetShares()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		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)
	}
}

var (
	metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")


M shell.nix => shell.nix +1 -1
@@ 16,4 16,4 @@
) {
  src =  ./.;
}).shellNix
# nix-direnv cache busting line: sha256-OfwliH0EmrGoOVog8b27F0p5+foElOrRZkOtGR4+Sts=
# nix-direnv cache busting line: sha256-eci4f6golU1eIQOezplA+I+gmOfof40ktIdpr0v/uMc=

M tailcfg/tailcfg.go => tailcfg/tailcfg.go +6 -1
@@ 1345,6 1345,8 @@ const (
	// PeerCapabilityWebUI grants the ability for a peer to edit features from the
	// device Web UI.
	PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui"
	// PeerCapabilityTailfs grants the ability for a peer to access tailfs shares.
	PeerCapabilityTailfs PeerCapability = "tailscale.com/cap/tailfs"
)

// NodeCapMap is a map of capabilities to their optional values. It is valid for


@@ 2087,7 2089,7 @@ const (
	CapabilitySSHRuleIn          NodeCapability = "https://tailscale.com/cap/ssh-rule-in"           // some SSH rule reach this node
	CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
	CapabilityDebug              NodeCapability = "https://tailscale.com/cap/debug"                 // exposes debug endpoints over the PeerAPI
	CapabilityHTTPS              NodeCapability = "https"                                           // https cert provisioning enabled on tailnet
	CapabilityHTTPS              NodeCapability = "https"

	// CapabilityBindToInterfaceByRoute changes how Darwin nodes create
	// sockets (in the net/netns package). See that package for more


@@ 2208,6 2210,9 @@ const (
	// NodeAttrProbeUDPLifetime makes the client probe UDP path lifetime at the
	// tail end of an active direct connection in magicsock.
	NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime"

	// NodeAttrsTailfsSharingEnabled enables sharing via Tailfs.
	NodeAttrsTailfsSharingEnabled NodeCapability = "tailfs:share"
)

// SetDNSRequest is a request to add a DNS record.

A tailfs/birthtiming.go => tailfs/birthtiming.go +83 -0
@@ 0,0 1,83 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tailfs

import (
	"context"
	"io/fs"
	"os"
	"time"

	"github.com/djherbis/times"
	"github.com/tailscale/xnet/webdav"
)

// birthTimingFS extends a webdav.FileSystem to return FileInfos that implement
// the webdav.BirthTimer interface.
type birthTimingFS struct {
	webdav.FileSystem
}

func (fs *birthTimingFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
	fi, err := fs.FileSystem.Stat(ctx, name)
	if err != nil {
		return nil, err
	}
	return &birthTimingFileInfo{fi}, nil
}

func (fs *birthTimingFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
	f, err := fs.FileSystem.OpenFile(ctx, name, flag, perm)
	if err != nil {
		return nil, err
	}

	return &birthTimingFile{f}, nil
}

// birthTimingFileInfo extends an os.FileInfo to implement the BirthTimer
// interface.
type birthTimingFileInfo struct {
	os.FileInfo
}

func (fi *birthTimingFileInfo) BirthTime(ctx context.Context) (time.Time, error) {
	if fi.Sys() == nil {
		return time.Time{}, webdav.ErrNotImplemented
	}

	if !times.HasBirthTime {
		return time.Time{}, webdav.ErrNotImplemented
	}

	return times.Get(fi.FileInfo).BirthTime(), nil
}

// birthTimingFile extends a webdav.File to return FileInfos that implement the
// BirthTimer interface.
type birthTimingFile struct {
	webdav.File
}

func (f *birthTimingFile) Stat() (fs.FileInfo, error) {
	fi, err := f.File.Stat()
	if err != nil {
		return nil, err
	}

	return &birthTimingFileInfo{fi}, nil
}

func (f *birthTimingFile) Readdir(count int) ([]fs.FileInfo, error) {
	fis, err := f.File.Readdir(count)
	if err != nil {
		return nil, err
	}

	for i, fi := range fis {
		fis[i] = &birthTimingFileInfo{fi}
	}

	return fis, nil
}

A tailfs/birthtiming_test.go => tailfs/birthtiming_test.go +104 -0
@@ 0,0 1,104 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// BirthTime is not supported on Linux, so only run the test on windows and Mac.

//go:build windows || darwin

package tailfs

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/tailscale/xnet/webdav"
)

func TestBirthTiming(t *testing.T) {
	ctx := context.Background()

	dir := t.TempDir()
	fs := &birthTimingFS{webdav.Dir(dir)}

	// create a file
	filename := "thefile"
	fullPath := filepath.Join(dir, filename)
	err := os.WriteFile(fullPath, []byte("hello beautiful world"), 0644)
	if err != nil {
		t.Fatalf("writing file failed: %s", err)
	}

	// wait a little bit
	time.Sleep(1 * time.Second)

	// append to the file to change its mtime
	file, err := os.OpenFile(fullPath, os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		t.Fatalf("opening file failed: %s", err)
	}
	_, err = file.Write([]byte("lookin' good!"))
	if err != nil {
		t.Fatalf("appending to file failed: %s", err)
	}
	err = file.Close()
	if err != nil {
		t.Fatalf("closing file failed: %s", err)
	}

	checkFileInfo := func(fi os.FileInfo) {
		if fi.ModTime().IsZero() {
			t.Fatal("FileInfo should have a non-zero ModTime")
		}
		bt, ok := fi.(webdav.BirthTimer)
		if !ok {
			t.Fatal("FileInfo should be a BirthTimer")
		}
		birthTime, err := bt.BirthTime(ctx)
		if err != nil {
			t.Fatalf("BirthTime() failed: %s", err)
		}
		if birthTime.IsZero() {
			t.Fatal("BirthTime() should return a non-zero time")
		}
		if !fi.ModTime().After(birthTime) {
			t.Fatal("ModTime() should be after BirthTime()")
		}
	}

	fi, err := fs.Stat(ctx, filename)
	if err != nil {
		t.Fatalf("statting file failed: %s", err)
	}
	checkFileInfo(fi)

	wfile, err := fs.OpenFile(ctx, filename, os.O_RDONLY, 0)
	if err != nil {
		t.Fatalf("opening file failed: %s", err)
	}
	defer wfile.Close()
	fi, err = wfile.Stat()
	if err != nil {
		t.Fatalf("statting file failed: %s", err)
	}
	if fi == nil {
		t.Fatal("statting file returned nil FileInfo")
	}
	checkFileInfo(fi)

	dfile, err := fs.OpenFile(ctx, ".", os.O_RDONLY, 0)
	if err != nil {
		t.Fatalf("opening directory failed: %s", err)
	}
	defer dfile.Close()
	fis, err := dfile.Readdir(0)
	if err != nil {
		t.Fatalf("readdir failed: %s", err)
	}
	if len(fis) != 1 {
		t.Fatalf("readdir should have returned 1 file info, but returned %d", 1)
	}
	checkFileInfo(fis[0])
}

A tailfs/compositefs/compositefs.go => tailfs/compositefs/compositefs.go +227 -0
@@ 0,0 1,227 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// Package compositefs provides a webdav.FileSystem that is composi
package compositefs

import (
	"io"
	"log"
	"os"
	"path"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/tailscale/xnet/webdav"
	"tailscale.com/tailfs/shared"
	"tailscale.com/tstime"
	"tailscale.com/types/logger"
)

// Child is a child filesystem of a CompositeFileSystem
type Child struct {
	// Name is the name of the child
	Name string
	// FS is the child's FileSystem
	FS webdav.FileSystem
	// Available is a function indicating whether or not the child is currently
	// available.
	Available func() bool
}

func (c *Child) isAvailable() bool {
	if c.Available == nil {
		return true
	}
	return c.Available()
}

// Options specifies options for configuring a CompositeFileSystem.
type Options struct {
	// Logf specifies a logging function to use
	Logf logger.Logf
	// StatChildren, if true, causes the CompositeFileSystem to stat its child
	// folders when generating a root directory listing. This gives more
	// accurate information but increases latency.
	StatChildren bool
	// Clock, if specified, determines the current time. If not specified, we
	// default to time.Now().
	Clock tstime.Clock
}

// New constructs a CompositeFileSystem that logs using the given logf.
func New(opts Options) *CompositeFileSystem {
	logf := opts.Logf
	if logf == nil {
		logf = log.Printf
	}
	fs := &CompositeFileSystem{
		logf:         logf,
		statChildren: opts.StatChildren,
	}
	if opts.Clock != nil {
		fs.now = opts.Clock.Now
	} else {
		fs.now = time.Now
	}
	return fs
}

// CompositeFileSystem is a webdav.FileSystem that is composed of multiple
// child webdav.FileSystems. Each child is identified by a name and appears
// as a folder within the root of the CompositeFileSystem, with the children
// sorted lexicographically by name.
//
// Children in a CompositeFileSystem can only be added or removed via calls to
// the AddChild and RemoveChild methods, they cannot be added via operations
// on the webdav.FileSystem interface like filesystem.Mkdir or filesystem.OpenFile.
// In other words, the root of the CompositeFileSystem acts as read-only, not
// permitting the addition, removal or renaming of folders.
//
// Rename is only supported within a single child. Renaming across children
// is not supported, as it wouldn't be possible to perform it atomically.
type CompositeFileSystem struct {
	logf         logger.Logf
	statChildren bool
	now          func() time.Time

	// childrenMu guards children
	childrenMu sync.Mutex
	children   []*Child
}

// AddChild ads a single child with the given name, replacing any existing
// child with the same name.
func (cfs *CompositeFileSystem) AddChild(child *Child) {
	cfs.childrenMu.Lock()
	oldIdx, oldChild := cfs.findChildLocked(child.Name)
	if oldChild != nil {
		// replace old child
		cfs.children[oldIdx] = child
	} else {
		// insert new child
		cfs.children = slices.Insert(cfs.children, oldIdx, child)
	}
	cfs.childrenMu.Unlock()

	if oldChild != nil {
		if c, ok := oldChild.FS.(io.Closer); ok {
			if err := c.Close(); err != nil {
				cfs.logf("closing child filesystem %v: %v", child.Name, err)
			}
		}
	}
}

// RemoveChild removes the child with the given name, if it exists.
func (cfs *CompositeFileSystem) RemoveChild(name string) {
	cfs.childrenMu.Lock()
	oldPos, oldChild := cfs.findChildLocked(name)
	if oldChild != nil {
		// remove old child
		copy(cfs.children[oldPos:], cfs.children[oldPos+1:])
		cfs.children = cfs.children[:len(cfs.children)-1]
	}
	cfs.childrenMu.Unlock()

	if oldChild != nil {
		closer, ok := oldChild.FS.(io.Closer)
		if ok {
			err := closer.Close()
			if err != nil {
				cfs.logf("failed to close child filesystem %v: %v", name, err)
			}
		}
	}
}

// SetChildren replaces the entire existing set of children with the given
// ones.
func (cfs *CompositeFileSystem) SetChildren(children ...*Child) {
	slices.SortFunc(children, func(a, b *Child) int {
		return strings.Compare(a.Name, b.Name)
	})

	cfs.childrenMu.Lock()
	oldChildren := cfs.children
	cfs.children = children
	cfs.childrenMu.Unlock()

	for _, child := range oldChildren {
		closer, ok := child.FS.(io.Closer)
		if ok {
			_ = closer.Close()
		}
	}
}

// GetChild returns the child with the given name and a boolean indicating
// whether or not it was found.
func (cfs *CompositeFileSystem) GetChild(name string) (webdav.FileSystem, bool) {
	_, child := cfs.findChildLocked(name)
	if child == nil {
		return nil, false
	}
	return child.FS, true
}

func (cfs *CompositeFileSystem) findChildLocked(name string) (int, *Child) {
	var child *Child
	i, found := slices.BinarySearchFunc(cfs.children, name, func(child *Child, name string) int {
		return strings.Compare(child.Name, name)
	})
	if found {
		child = cfs.children[i]
	}
	return i, child
}

// pathInfoFor returns a pathInfo for the given filename. If the filename
// refers to a Child that does not exist within this CompositeFileSystem,
// it will return the error os.ErrNotExist. Even when returning an error,
// it will still return a complete pathInfo.
func (cfs *CompositeFileSystem) pathInfoFor(name string) (pathInfo, error) {
	cfs.childrenMu.Lock()
	defer cfs.childrenMu.Unlock()

	var info pathInfo
	pathComponents := shared.CleanAndSplit(name)
	_, info.child = cfs.findChildLocked(pathComponents[0])
	info.refersToChild = len(pathComponents) == 1
	if !info.refersToChild {
		info.pathOnChild = path.Join(pathComponents[1:]...)
	}
	if info.child == nil {
		return info, os.ErrNotExist
	}
	return info, nil
}

// pathInfo provides information about a path
type pathInfo struct {
	// child is the Child corresponding to the first component of the path.
	child *Child
	// refersToChild indicates that that path refers directly to the child
	// (i.e. the path has only 1 component).
	refersToChild bool
	// pathOnChild is the path within the child (i.e. path minus leading component)
	// if and only if refersToChild is false.
	pathOnChild string
}

func (cfs *CompositeFileSystem) Close() error {
	cfs.childrenMu.Lock()
	children := cfs.children
	cfs.childrenMu.Unlock()

	for _, child := range children {
		closer, ok := child.FS.(io.Closer)
		if ok {
			_ = closer.Close()
		}
	}

	return nil
}

A tailfs/compositefs/compositefs_test.go => tailfs/compositefs/compositefs_test.go +497 -0
@@ 0,0 1,497 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositefs

import (
	"context"
	"errors"
	"io/fs"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/tailscale/xnet/webdav"
	"tailscale.com/tailfs/shared"
	"tailscale.com/tstest"
)

func TestStat(t *testing.T) {
	cfs, dir1, _, clock, close := createFileSystem(t, nil)
	defer close()

	tests := []struct {
		label    string
		name     string
		expected fs.FileInfo
		err      error
	}{
		{
			label: "root folder",
			name:  "/",
			expected: &shared.StaticFileInfo{
				Named:      "/",
				Sized:      0,
				ModdedTime: clock.Now(),
				Dir:        true,
			},
		},
		{
			label: "remote1",
			name:  "/remote1",
			expected: &shared.StaticFileInfo{
				Named:      "/remote1",
				Sized:      0,
				ModdedTime: clock.Now(),
				Dir:        true,
			},
		},
		{
			label: "remote2",
			name:  "/remote2",
			expected: &shared.StaticFileInfo{
				Named:      "/remote2",
				Sized:      0,
				ModdedTime: clock.Now(),
				Dir:        true,
			},
		},
		{
			label: "non-existent remote",
			name:  "/remote3",
			err:   os.ErrNotExist,
		},
		{
			label: "file on remote1",
			name:  "/remote1/file1.txt",
			expected: &shared.StaticFileInfo{
				Named:      "/remote1/file1.txt",
				Sized:      stat(t, filepath.Join(dir1, "file1.txt")).Size(),
				ModdedTime: stat(t, filepath.Join(dir1, "file1.txt")).ModTime(),
				Dir:        false,
			},
		},
	}

	ctx := context.Background()
	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			fi, err := cfs.Stat(ctx, test.name)
			if test.err != nil {
				if err == nil || !errors.Is(err, test.err) {
					t.Errorf("expected error: %v   got: %v", test.err, err)
				}
			} else {
				if err != nil {
					t.Errorf("unable to stat file: %v", err)
				} else {
					infosEqual(t, test.expected, fi)
				}
			}
		})
	}
}

func TestStatWithStatChildren(t *testing.T) {
	cfs, dir1, dir2, _, close := createFileSystem(t, &Options{StatChildren: true})
	defer close()

	tests := []struct {
		label    string
		name     string
		expected fs.FileInfo
	}{
		{
			label: "root folder",
			name:  "/",
			expected: &shared.StaticFileInfo{
				Named:      "/",
				Sized:      0,
				ModdedTime: stat(t, dir2).ModTime(), // ModTime should be greatest modtime of children
				Dir:        true,
			},
		},
		{
			label: "remote1",
			name:  "/remote1",
			expected: &shared.StaticFileInfo{
				Named:      "/remote1",
				Sized:      stat(t, dir1).Size(),
				ModdedTime: stat(t, dir1).ModTime(),
				Dir:        true,
			},
		},
		{
			label: "remote2",
			name:  "/remote2",
			expected: &shared.StaticFileInfo{
				Named:      "/remote2",
				Sized:      stat(t, dir2).Size(),
				ModdedTime: stat(t, dir2).ModTime(),
				Dir:        true,
			},
		},
	}

	ctx := context.Background()
	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			fi, err := cfs.Stat(ctx, test.name)
			if err != nil {
				t.Errorf("unable to stat file: %v", err)
			} else {
				infosEqual(t, test.expected, fi)
			}
		})
	}
}

func TestMkdir(t *testing.T) {
	fs, _, _, _, close := createFileSystem(t, nil)
	defer close()

	tests := []struct {
		label string
		name  string
		perm  os.FileMode
		err   error
	}{
		{
			label: "attempt to create root folder",
			name:  "/",
		},
		{
			label: "attempt to create remote",
			name:  "/remote1",
		},
		{
			label: "attempt to create non-existent remote",
			name:  "/remote3",
			err:   os.ErrPermission,
		},
		{
			label: "attempt to create file on non-existent remote",
			name:  "/remote3/somefile.txt",
			err:   os.ErrNotExist,
		},
		{
			label: "success",
			name:  "/remote1/newfile.txt",
			perm:  0772,
		},
	}

	ctx := context.Background()
	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			err := fs.Mkdir(ctx, test.name, test.perm)
			if test.err != nil {
				if err == nil || !errors.Is(err, test.err) {
					t.Errorf("expected error: %v   got: %v", test.err, err)
				}
			} else {
				if err != nil {
					t.Errorf("unexpected error: %v", err)
				} else {
					fi, err := fs.Stat(ctx, test.name)
					if err != nil {
						t.Errorf("unable to stat file: %v", err)
					} else {
						if fi.Name() != test.name {
							t.Errorf("expected name: %v   got: %v", test.name, fi.Name())
						}
						if !fi.IsDir() {
							t.Error("expected directory")
						}
					}
				}
			}
		})
	}
}

func TestRemoveAll(t *testing.T) {
	fs, _, _, _, close := createFileSystem(t, nil)
	defer close()

	tests := []struct {
		label string
		name  string
		err   error
	}{
		{
			label: "attempt to remove root folder",
			name:  "/",
			err:   os.ErrPermission,
		},
		{
			label: "attempt to remove remote",
			name:  "/remote1",
			err:   os.ErrPermission,
		},
		{
			label: "attempt to remove non-existent remote",
			name:  "/remote3",
			err:   os.ErrPermission,
		},
		{
			label: "attempt to remove file on non-existent remote",
			name:  "/remote3/somefile.txt",
			err:   os.ErrNotExist,
		},
		{
			label: "remove non-existent file",
			name:  "/remote1/nonexistent.txt",
		},
		{
			label: "remove existing file",
			name:  "/remote1/dir1",
		},
	}

	ctx := context.Background()
	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			err := fs.RemoveAll(ctx, test.name)
			if test.err != nil {
				if err == nil || !errors.Is(err, test.err) {
					t.Errorf("expected error: %v   got: %v", test.err, err)
				}
			} else {
				if err != nil {
					t.Errorf("unexpected error: %v", err)
				} else {
					_, err := fs.Stat(ctx, test.name)
					if !os.IsNotExist(err) {
						t.Errorf("expected dir to be gone: %v", err)
					}
				}
			}
		})
	}
}

func TestRename(t *testing.T) {
	fs, _, _, _, close := createFileSystem(t, nil)
	defer close()

	tests := []struct {
		label           string
		oldName         string
		newName         string
		err             error
		expectedNewInfo *shared.StaticFileInfo
	}{
		{
			label:   "attempt to move root folder",
			oldName: "/",
			newName: "/remote2/copy.txt",
			err:     os.ErrPermission,
		},
		{
			label:   "attempt to move to root folder",
			oldName: "/remote1/file1.txt",
			newName: "/",
			err:     os.ErrPermission,
		},
		{
			label:   "attempt to move to remote",
			oldName: "/remote1/file1.txt",
			newName: "/remote2",
			err:     os.ErrPermission,
		},
		{
			label:   "attempt to move to non-existent remote",
			oldName: "/remote1/file1.txt",
			newName: "/remote3",
			err:     os.ErrPermission,
		},
		{
			label:   "attempt to move file from non-existent remote",
			oldName: "/remote3/file1.txt",
			newName: "/remote1/file1.txt",
			err:     os.ErrNotExist,
		},
		{
			label:   "attempt to move file to a non-existent remote",
			oldName: "/remote2/file2.txt",
			newName: "/remote3/file2.txt",
			err:     os.ErrNotExist,
		},
		{
			label:   "attempt to move file across remotes",
			oldName: "/remote1/file1.txt",
			newName: "/remote2/file1.txt",
			err:     os.ErrPermission,
		},
		{
			label:   "attempt to move remote itself",
			oldName: "/remote1",
			newName: "/remote2",
			err:     os.ErrPermission,
		},
		{
			label:   "attempt to move to a remote",
			oldName: "/remote1/file2.txt",
			newName: "/remote2",
			err:     os.ErrPermission,
		},
		{
			label:   "move file within remote",
			oldName: "/remote2/file2.txt",
			newName: "/remote2/file3.txt",
			expectedNewInfo: &shared.StaticFileInfo{
				Named: "/remote2/file3.txt",
				Sized: 5,
				Dir:   false,
			},
		},
	}

	ctx := context.Background()
	for _, test := range tests {
		t.Run(test.label, func(t *testing.T) {
			err := fs.Rename(ctx, test.oldName, test.newName)
			if test.err != nil {
				if err == nil || test.err.Error() != err.Error() {
					t.Errorf("expected error: %v   got: %v", test.err, err)
				}
			} else {
				if err != nil {
					t.Errorf("unexpected error: %v", err)
				} else {
					fi, err := fs.Stat(ctx, test.newName)
					if err != nil {
						t.Errorf("unexpected error: %v", err)
					} else {
						// Override modTime to avoid having to compare it
						test.expectedNewInfo.ModdedTime = fi.ModTime()
						infosEqual(t, test.expectedNewInfo, fi)
					}
				}
			}
		})
	}
}

func createFileSystem(t *testing.T, opts *Options) (webdav.FileSystem, string, string, *tstest.Clock, func()) {
	l1, dir1 := startRemote(t)
	l2, dir2 := startRemote(t)

	// Make some files, use perms 0666 as lowest common denominator that works
	// on both UNIX and Windows.
	err := os.WriteFile(filepath.Join(dir1, "file1.txt"), []byte("12345"), 0666)
	if err != nil {
		t.Fatal(err)
	}
	err = os.WriteFile(filepath.Join(dir2, "file2.txt"), []byte("54321"), 0666)
	if err != nil {
		t.Fatal(err)
	}

	// make some directories
	err = os.Mkdir(filepath.Join(dir1, "dir1"), 0666)
	if err != nil {
		t.Fatal(err)
	}
	err = os.Mkdir(filepath.Join(dir2, "dir2"), 0666)
	if err != nil {
		t.Fatal(err)
	}

	if opts == nil {
		opts = &Options{}
	}
	if opts.Logf == nil {
		opts.Logf = t.Logf
	}
	clock := tstest.NewClock(tstest.ClockOpts{Start: time.Now()})
	opts.Clock = clock

	fs := New(*opts)
	fs.AddChild(&Child{Name: "remote4", FS: &closeableFS{webdav.Dir(dir2)}})
	fs.SetChildren(&Child{Name: "remote2", FS: webdav.Dir(dir2)},
		&Child{Name: "remote3", FS: &closeableFS{webdav.Dir(dir2)}},
	)
	fs.AddChild(&Child{Name: "remote1", FS: webdav.Dir(dir1)})
	fs.RemoveChild("remote3")

	child, ok := fs.GetChild("remote1")
	if !ok || child == nil {
		t.Fatal("unable to GetChild(remote1)")
	}
	child, ok = fs.GetChild("remote2")
	if !ok || child == nil {
		t.Fatal("unable to GetChild(remote2)")
	}
	child, ok = fs.GetChild("remote3")
	if ok || child != nil {
		t.Fatal("should have been able to GetChild(remote3)")
	}
	child, ok = fs.GetChild("remote4")
	if ok || child != nil {
		t.Fatal("should have been able to GetChild(remote4)")
	}

	return fs, dir1, dir2, clock, func() {
		defer l1.Close()
		defer os.RemoveAll(dir1)
		defer l2.Close()
		defer os.RemoveAll(dir2)
	}
}

func stat(t *testing.T, path string) fs.FileInfo {
	fi, err := os.Stat(path)
	if err != nil {
		t.Fatal(err)
	}
	return fi
}

func startRemote(t *testing.T) (net.Listener, string) {
	dir := t.TempDir()

	l, err := net.Listen("tcp", "127.0.0.1:")
	if err != nil {
		t.Fatal(err)
	}

	h := &webdav.Handler{
		FileSystem: webdav.Dir(dir),
		LockSystem: webdav.NewMemLS(),
	}

	s := &http.Server{Handler: h}
	go s.Serve(l)

	return l, dir
}

func infosEqual(t *testing.T, expected, actual fs.FileInfo) {
	t.Helper()
	if expected.Name() != actual.Name() {
		t.Errorf("expected name: %v   got: %v", expected.Name(), actual.Name())
	}
	if expected.Size() != actual.Size() {
		t.Errorf("expected Size: %v   got: %v", expected.Size(), actual.Size())
	}
	if !expected.ModTime().Truncate(time.Second).UTC().Equal(actual.ModTime().Truncate(time.Second).UTC()) {
		t.Errorf("expected ModTime: %v   got: %v", expected.ModTime(), actual.ModTime())
	}
	if expected.IsDir() != actual.IsDir() {
		t.Errorf("expected IsDir: %v   got: %v", expected.IsDir(), actual.IsDir())
	}
}

// closeableFS is a webdav.FileSystem that implements io.Closer()
type closeableFS struct {
	webdav.FileSystem
}

func (cfs *closeableFS) Close() error {
	return nil
}

A tailfs/compositefs/mkdir.go => tailfs/compositefs/mkdir.go +39 -0
@@ 0,0 1,39 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositefs

import (
	"context"
	"os"

	"tailscale.com/tailfs/shared"
)

// Mkdir implements webdav.Filesystem. The root of this file system is
// read-only, so any attempts to make directories within the root will fail
// with os.ErrPermission. Attempts to make directories within one of the child
// filesystems will be handled by the respective child.
func (cfs *CompositeFileSystem) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
	if shared.IsRoot(name) {
		// root directory already exists, consider this okay
		return nil
	}

	pathInfo, err := cfs.pathInfoFor(name)
	if pathInfo.refersToChild {
		// children can't be made
		if pathInfo.child != nil {
			// since child already exists, consider this okay
			return nil
		}
		// since child doesn't exist, return permission error
		return os.ErrPermission
	}

	if err != nil {
		return err
	}

	return pathInfo.child.FS.Mkdir(ctx, pathInfo.pathOnChild, perm)
}

A tailfs/compositefs/openfile.go => tailfs/compositefs/openfile.go +65 -0
@@ 0,0 1,65 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositefs

import (
	"context"
	"io/fs"
	"os"

	"github.com/tailscale/xnet/webdav"
	"tailscale.com/tailfs/shared"
)

// OpenFile implements interface webdav.Filesystem.
func (cfs *CompositeFileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
	if !shared.IsRoot(name) {
		pathInfo, err := cfs.pathInfoFor(name)
		if err != nil {
			return nil, err
		}

		if pathInfo.refersToChild {
			// this is the child itself, ask it to open its root
			return pathInfo.child.FS.OpenFile(ctx, "/", flag, perm)
		}

		return pathInfo.child.FS.OpenFile(ctx, pathInfo.pathOnChild, flag, perm)
	}

	// the root directory contains one directory for each child
	di, err := cfs.Stat(ctx, name)
	if err != nil {
		return nil, err
	}

	return &shared.DirFile{
		Info: di,
		LoadChildren: func() ([]fs.FileInfo, error) {
			cfs.childrenMu.Lock()
			children := cfs.children
			cfs.childrenMu.Unlock()

			childInfos := make([]fs.FileInfo, 0, len(cfs.children))
			for _, c := range children {
				if c.isAvailable() {
					var childInfo fs.FileInfo
					if cfs.statChildren {
						fi, err := c.FS.Stat(ctx, "/")
						if err != nil {
							return nil, err
						}
						// we use the full name
						childInfo = shared.RenamedFileInfo(ctx, c.Name, fi)
					} else {
						// always use now() as the modified time to bust caches
						childInfo = shared.ReadOnlyDirInfo(c.Name, cfs.now())
					}
					childInfos = append(childInfos, childInfo)
				}
			}
			return childInfos, nil
		},
	}, nil
}

A tailfs/compositefs/removeall.go => tailfs/compositefs/removeall.go +33 -0
@@ 0,0 1,33 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositefs

import (
	"context"
	"os"

	"tailscale.com/tailfs/shared"
)

// RemoveAll implements webdav.File. The root of this file system is read-only,
// so attempting to call RemoveAll on the root will fail with os.ErrPermission.
// RemoveAll within a child will be handled by the respective child.
func (cfs *CompositeFileSystem) RemoveAll(ctx context.Context, name string) error {
	if shared.IsRoot(name) {
		// root directory is read-only
		return os.ErrPermission
	}

	pathInfo, err := cfs.pathInfoFor(name)
	if pathInfo.refersToChild {
		// children can't be removed
		return os.ErrPermission
	}

	if err != nil {
		return err
	}

	return pathInfo.child.FS.RemoveAll(ctx, pathInfo.pathOnChild)
}

A tailfs/compositefs/rename.go => tailfs/compositefs/rename.go +49 -0
@@ 0,0 1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositefs

import (
	"context"
	"os"

	"tailscale.com/tailfs/shared"
)

// Rename implements interface webdav.FileSystem. The root of this file system
// is read-only, so any attempt to rename a child within the root of this
// filesystem will fail with os.ErrPermission. Renaming across children is not
// supported and will fail with os.ErrPermission. Renaming within a child will
// be handled by the respective child.
func (cfs *CompositeFileSystem) Rename(ctx context.Context, oldName, newName string) error {
	if shared.IsRoot(oldName) || shared.IsRoot(newName) {
		// root directory is read-only
		return os.ErrPermission
	}

	oldPathInfo, err := cfs.pathInfoFor(oldName)
	if oldPathInfo.refersToChild {
		// children themselves are read-only
		return os.ErrPermission
	}
	if err != nil {
		return err
	}

	newPathInfo, err := cfs.pathInfoFor(newName)
	if newPathInfo.refersToChild {
		// children themselves are read-only
		return os.ErrPermission
	}
	if err != nil {
		return err
	}

	if oldPathInfo.child != newPathInfo.child {
		// moving a file across children is not permitted
		return os.ErrPermission
	}

	// file is moving within the same child, let the child handle it
	return oldPathInfo.child.FS.Rename(ctx, oldPathInfo.pathOnChild, newPathInfo.pathOnChild)
}

A tailfs/compositefs/stat.go => tailfs/compositefs/stat.go +55 -0
@@ 0,0 1,55 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositefs

import (
	"context"
	"io/fs"

	"tailscale.com/tailfs/shared"
)

// Stat implements webdav.FileSystem.
func (cfs *CompositeFileSystem) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
	if shared.IsRoot(name) {
		// Root is a directory
		// always use now() as the modified time to bust caches
		fi := shared.ReadOnlyDirInfo(name, cfs.now())
		if cfs.statChildren {
			// update last modified time based on children
			cfs.childrenMu.Lock()
			children := cfs.children
			cfs.childrenMu.Unlock()
			for i, child := range children {
				childInfo, err := child.FS.Stat(ctx, "/")
				if err != nil {
					return nil, err
				}
				if i == 0 || childInfo.ModTime().After(fi.ModTime()) {
					fi.ModdedTime = childInfo.ModTime()
				}
			}
		}
		return fi, nil
	}

	pathInfo, err := cfs.pathInfoFor(name)
	if err != nil {
		return nil, err
	}

	if pathInfo.refersToChild && !cfs.statChildren {
		// Return a read-only FileInfo for this child.
		// Always use now() as the modified time to bust caches.
		return shared.ReadOnlyDirInfo(name, cfs.now()), nil
	}

	fi, err := pathInfo.child.FS.Stat(ctx, pathInfo.pathOnChild)
	if err != nil {
		return nil, err
	}

	// we use the full name, which is different than what the child sees
	return shared.RenamedFileInfo(ctx, name, fi), nil
}

A tailfs/connlistener.go => tailfs/connlistener.go +79 -0
@@ 0,0 1,79 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tailfs

import (
	"log"
	"net"
	"sync"
	"syscall"
)

type connListener struct {
	ch       chan net.Conn
	closedCh chan any
	closeMu  sync.Mutex
}

// newConnListener creates a net.Listener to which one can hand connections
// directly.
func newConnListener() *connListener {
	return &connListener{
		ch:       make(chan net.Conn),
		closedCh: make(chan any),
	}
}

func (l *connListener) Accept() (net.Conn, error) {
	select {
	case <-l.closedCh:
		// TODO(oxtoacart): make this error match what a regular net.Listener does
		return nil, syscall.EINVAL
	case conn := <-l.ch:
		return conn, nil
	}
}

// Addr implements net.Listener. This always returns nil. It is assumed that
// this method is currently unused, so it logs a warning if it ever does get
// called.
func (l *connListener) Addr() net.Addr {
	log.Println("warning: unexpected call to connListener.Addr()")
	return nil
}

func (l *connListener) Close() error {
	l.closeMu.Lock()
	defer l.closeMu.Unlock()

	select {
	case <-l.closedCh:
		// Already closed.
		return syscall.EINVAL
	default:
		// We don't close l.ch because someone maybe trying to send to that,
		// which would cause a panic.
		close(l.closedCh)
		return nil
	}
}

func (l *connListener) HandleConn(c net.Conn, remoteAddr net.Addr) error {
	select {
	case <-l.closedCh:
		return syscall.EINVAL
	case l.ch <- &connWithRemoteAddr{Conn: c, remoteAddr: remoteAddr}:
		// Connection has been accepted.
	}
	return nil
}

type connWithRemoteAddr struct {
	net.Conn
	remoteAddr net.Addr
}

func (c *connWithRemoteAddr) RemoteAddr() net.Addr {
	return c.remoteAddr
}

A tailfs/connlistener_test.go => tailfs/connlistener_test.go +68 -0
@@ 0,0 1,68 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tailfs

import (
	"log"
	"net"
	"testing"
)

func TestConnListener(t *testing.T) {
	l, err := net.Listen("tcp", "127.0.0.1:")
	if err != nil {
		t.Fatalf("failed to Listen: %s", err)
	}

	cl := newConnListener()
	// Test that we can accept a connection
	cc, err := net.Dial("tcp", l.Addr().String())
	if err != nil {
		t.Fatalf("failed to Dial: %s", err)
	}
	defer cc.Close()

	sc, err := l.Accept()
	if err != nil {
		t.Fatalf("failed to Accept: %s", err)
	}

	remoteAddr := &net.TCPAddr{IP: net.ParseIP("10.10.10.10"), Port: 1234}
	go func() {
		err := cl.HandleConn(sc, remoteAddr)
		if err != nil {
			log.Printf("failed to HandleConn: %s", err)
		}
	}()

	clc, err := cl.Accept()
	if err != nil {
		t.Fatalf("failed to Accept: %s", err)
	}
	defer clc.Close()

	if clc.RemoteAddr().String() != remoteAddr.String() {
		t.Fatalf("ConnListener accepted the wrong connection, got %q, want %q", clc.RemoteAddr(), remoteAddr)
	}

	err = cl.Close()
	if err != nil {
		t.Fatalf("failed to Close: %s", err)
	}

	err = cl.Close()
	if err == nil {
		t.Fatal("should have failed on second Close")
	}

	err = cl.HandleConn(sc, remoteAddr)
	if err == nil {
		t.Fatal("should have failed on HandleConn after Close")
	}

	_, err = cl.Accept()
	if err == nil {
		t.Fatal("should have failed on Accept after Close")
	}
}

A tailfs/fileserver.go => tailfs/fileserver.go +115 -0
@@ 0,0 1,115 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tailfs

import (
	"net"
	"net/http"
	"sync"

	"github.com/tailscale/xnet/webdav"
	"tailscale.com/tailfs/shared"
)

// FileServer is a standalone WebDAV server that dynamically serves up shares.
// It's typically used in a separate process from the actual Tailfs server to
// serve up files as an unprivileged user.
type FileServer struct {
	l             net.Listener
	shareHandlers map[string]http.Handler
	sharesMu      sync.RWMutex
}

// NewFileServer constructs a FileServer.
//
// The server attempts to listen at a random address on 127.0.0.1.
// The listen address is available via the Addr() method.
//
// The server has to be told about shares before it can serve them. This is
// accomplished either by calling SetShares(), or locking the shares with
// LockShares(), clearing them with ClearSharesLocked(), adding them
// individually with AddShareLocked(), and finally unlocking them with
// UnlockShares().
//
// The server doesn't actually process requests until the Serve() method is
// called.
func NewFileServer() (*FileServer, error) {
	// path := filepath.Join(os.TempDir(), fmt.Sprintf("%v.socket", uuid.New().String()))
	// l, err := safesocket.Listen(path)
	// if err != nil {
	// TODO(oxtoacart): actually get safesocket working in more environments (MacOS Sandboxed, Windows, ???)
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return nil, err
	}
	// }
	return &FileServer{
		l:             l,
		shareHandlers: make(map[string]http.Handler),
	}, nil
}

// Addr returns the address at which this FileServer is listening.
func (s *FileServer) Addr() string {
	return s.l.Addr().String()
}

// Serve() starts serving files and blocks until it encounters a fatal error.
func (s *FileServer) Serve() error {
	return http.Serve(s.l, s)
}

// LockShares locks the map of shares in preparation for manipulating it.
func (s *FileServer) LockShares() {
	s.sharesMu.Lock()
}

// UnlockShares unlocks the map of shares.
func (s *FileServer) UnlockShares() {
	s.sharesMu.Unlock()
}

// ClearSharesLocked clears the map of shares, assuming that LockShares() has
// been called first.
func (s *FileServer) ClearSharesLocked() {
	s.shareHandlers = make(map[string]http.Handler)
}

// AddShareLocked adds a share to the map of shares, assuming that LockShares()
// has been called first.
func (s *FileServer) AddShareLocked(share, path string) {
	s.shareHandlers[share] = &webdav.Handler{
		FileSystem: &birthTimingFS{webdav.Dir(path)},
		LockSystem: webdav.NewMemLS(),
	}
}

// SetShares sets the full map of shares to the new value, mapping name->path.
func (s *FileServer) SetShares(shares map[string]string) {
	s.LockShares()
	defer s.UnlockShares()
	s.ClearSharesLocked()
	for name, path := range shares {
		s.AddShareLocked(name, path)
	}
}

// ServeHTTP implements the http.Handler interface.
func (s *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	parts := shared.CleanAndSplit(r.URL.Path)
	r.URL.Path = shared.Join(parts[1:]...)
	share := parts[0]
	s.sharesMu.RLock()
	h, found := s.shareHandlers[share]
	s.sharesMu.RUnlock()
	if !found {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	h.ServeHTTP(w, r)
}

func (s *FileServer) Close() error {
	return s.l.Close()
}

A tailfs/local.go => tailfs/local.go +99 -0
@@ 0,0 1,99 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tailfs

import (
	"log"
	"net"
	"net/http"

	"github.com/tailscale/xnet/webdav"
	"tailscale.com/tailfs/compositefs"
	"tailscale.com/tailfs/webdavfs"
	"tailscale.com/types/logger"
)

// Remote represents a remote Tailfs node.
type Remote struct {
	Name      string
	URL       string
	Available func() bool
}

// NewFileSystemForLocal starts serving a filesystem for local clients.
// Inbound connections must be handed to HandleConn.
func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
	if logf == nil {
		logf = log.Printf
	}
	fs := &FileSystemForLocal{
		logf:     logf,
		cfs:      compositefs.New(compositefs.Options{Logf: logf}),
		listener: newConnListener(),
	}
	fs.startServing()
	return fs
}

// FileSystemForLocal is the Tailfs filesystem exposed to local clients. It
// provides a unified WebDAV interface to remote Tailfs shares on other nodes.
type FileSystemForLocal struct {
	logf     logger.Logf
	cfs      *compositefs.CompositeFileSystem
	listener *connListener
}

func (s *FileSystemForLocal) startServing() {
	hs := &http.Server{
		Handler: &webdav.Handler{
			FileSystem: s.cfs,
			LockSystem: webdav.NewMemLS(),
		},
	}
	go func() {
		err := hs.Serve(s.listener)
		if err != nil {
			// TODO(oxtoacart): should we panic or something different here?
			log.Printf("serve: %v", err)
		}
	}()
}

// HandleConn handles connections from local WebDAV clients
func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) error {
	return s.listener.HandleConn(conn, remoteAddr)
}

// SetRemotes sets the complete set of remotes on the given tailnet domain
// using a map of name -> url. If transport is specified, that transport
// will be used to connect to these remotes.
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper) {
	children := make([]*compositefs.Child, 0, len(remotes))
	for _, remote := range remotes {
		opts := webdavfs.Options{
			URL:          remote.URL,
			Transport:    transport,
			StatCacheTTL: statCacheTTL,
			Logf:         s.logf,
		}
		children = append(children, &compositefs.Child{
			Name:      remote.Name,
			FS:        webdavfs.New(opts),
			Available: remote.Available,
		})
	}

	domainChild, found := s.cfs.GetChild(domain)
	if !found {
		domainChild = compositefs.New(compositefs.Options{Logf: s.logf})
		s.cfs.SetChildren(&compositefs.Child{Name: domain, FS: domainChild})
	}
	domainChild.(*compositefs.CompositeFileSystem).SetChildren(children...)
}

// Close() stops serving the WebDAV content
func (s *FileSystemForLocal) Close() error {
	s.cfs.Close()
	return s.listener.Close()
}

A tailfs/remote.go => tailfs/remote.go +389 -0
@@ 0,0 1,389 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package tailfs

import (
	"bufio"
	"encoding/hex"
	"fmt"
	"log"
	"math"
	"net"
	"net/http"
	"net/netip"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"

	"github.com/tailscale/xnet/webdav"
	"tailscale.com/safesocket"
	"tailscale.com/tailfs/compositefs"
	"tailscale.com/tailfs/shared"
	"tailscale.com/tailfs/webdavfs"
	"tailscale.com/types/logger"
)

var (
	disallowShareAs = false
)

// AllowShareAs reports whether sharing files as a specific user is allowed.
func AllowShareAs() bool {
	return !disallowShareAs && doAllowShareAs()
}

// Share represents a folder that's shared with remote Tailfs nodes.
type Share struct {
	// Name is how this share appears on remote nodes.
	Name string `json:"name"`
	// Path is the path to the directory on this machine that's being shared.
	Path string `json:"path"`
	// As is the UNIX or Windows username of the local account used for this
	// share. File read/write permissions are enforced based on this username.
	As string `json:"who"`
}

func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
	if logf == nil {
		logf = log.Printf
	}
	fs := &FileSystemForRemote{
		logf:        logf,
		lockSystem:  webdav.NewMemLS(),
		fileSystems: make(map[string]webdav.FileSystem),
		userServers: make(map[string]*userServer),
	}
	return fs
}

// FileSystemForRemote is the Tailfs filesystem exposed to remote nodes. It
// provides a unified WebDAV interface to local directories that have been
// shared.
type FileSystemForRemote struct {
	logf       logger.Logf
	lockSystem webdav.LockSystem

	// mu guards the below values. Acquire a write lock before updating any of
	// them, acquire a read lock before reading any of them.
	mu             sync.RWMutex
	fileServerAddr string
	shares         map[string]*Share
	fileSystems    map[string]webdav.FileSystem
	userServers    map[string]*userServer
}

// SetFileServerAddr sets the address of the file server to which we
// should proxy. This is used on platforms like Windows and MacOS
// sandboxed where we can't spawn user-specific sub-processes and instead
// rely on the UI application that's already running as an unprivileged
// user to access the filesystem for us.
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
	s.mu.Lock()
	s.fileServerAddr = addr
	s.mu.Unlock()
}

// SetShares sets the complete set of shares exposed by this node. If
// AllowShareAs() reports true, we will use one subprocess per user to
// access the filesystem (see userServer). Otherwise, we will use the file
// server configured via SetFileServerAddr.
func (s *FileSystemForRemote) SetShares(shares map[string]*Share) {
	userServers := make(map[string]*userServer)
	if AllowShareAs() {
		// set up per-user server
		for _, share := range shares {
			p, found := userServers[share.As]
			if !found {
				p = &userServer{
					logf: s.logf,
				}
				userServers[share.As] = p
			}
			p.shares = append(p.shares, share)
		}
		for _, p := range userServers {
			go p.runLoop()
		}
	}

	fileSystems := make(map[string]webdav.FileSystem, len(shares))
	for _, share := range shares {
		fileSystems[share.Name] = s.buildWebDAVFS(share)
	}

	s.mu.Lock()
	s.shares = shares
	oldFileSystems := s.fileSystems
	oldUserServers := s.userServers
	s.fileSystems = fileSystems