Browse Source

tailfs: clean up naming and package structure

- Restyles tailfs -> tailFS
- Defines interfaces for main TailFS types
- Moves implemenatation of TailFS into tailfsimpl package

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <[email protected]>
Percy Wegmann 2 years ago
parent
commit
abab0d4197
50 changed files with 754 additions and 684 deletions
  1. 10 10
      client/tailscale/localclient.go
  2. 16 1
      client/tailscale/localclient_test.go
  3. 1 12
      cmd/derper/depaware.txt
  4. 3 3
      cmd/tailscale/cli/share.go
  5. 3 13
      cmd/tailscale/depaware.txt
  6. 8 7
      cmd/tailscaled/depaware.txt
  7. 15 13
      cmd/tailscaled/tailscaled.go
  8. 7 6
      ipn/backend.go
  9. 24 24
      ipn/ipnlocal/local.go
  10. 9 11
      ipn/ipnlocal/peerapi.go
  11. 52 47
      ipn/ipnlocal/tailfs.go
  12. 8 8
      ipn/localapi/localapi.go
  13. 4 4
      tailcfg/tailcfg.go
  14. 12 0
      tailcfg/tailcfg_test.go
  15. 18 80
      tailfs/local.go
  16. 31 360
      tailfs/remote.go
  17. 0 18
      tailfs/tailfs.go
  18. 1 1
      tailfs/tailfsimpl/birthtiming.go
  19. 1 1
      tailfs/tailfsimpl/birthtiming_test.go
  20. 1 1
      tailfs/tailfsimpl/compositefs/compositefs.go
  21. 1 1
      tailfs/tailfsimpl/compositefs/compositefs_test.go
  22. 1 1
      tailfs/tailfsimpl/compositefs/mkdir.go
  23. 1 1
      tailfs/tailfsimpl/compositefs/openfile.go
  24. 1 1
      tailfs/tailfsimpl/compositefs/removeall.go
  25. 1 1
      tailfs/tailfsimpl/compositefs/rename.go
  26. 1 1
      tailfs/tailfsimpl/compositefs/stat.go
  27. 1 1
      tailfs/tailfsimpl/connlistener.go
  28. 1 1
      tailfs/tailfsimpl/connlistener_test.go
  29. 3 3
      tailfs/tailfsimpl/fileserver.go
  30. 103 0
      tailfs/tailfsimpl/local_impl.go
  31. 359 0
      tailfs/tailfsimpl/remote_impl.go
  32. 0 0
      tailfs/tailfsimpl/shared/pathutil.go
  33. 0 0
      tailfs/tailfsimpl/shared/pathutil_test.go
  34. 0 0
      tailfs/tailfsimpl/shared/readonlydir.go
  35. 0 0
      tailfs/tailfsimpl/shared/stat.go
  36. 19 18
      tailfs/tailfsimpl/tailfs_test.go
  37. 0 0
      tailfs/tailfsimpl/webdavfs/readonly_file.go
  38. 0 0
      tailfs/tailfsimpl/webdavfs/stat_cache.go
  39. 1 1
      tailfs/tailfsimpl/webdavfs/stat_cache_test.go
  40. 1 1
      tailfs/tailfsimpl/webdavfs/webdavfs.go
  41. 1 1
      tailfs/tailfsimpl/webdavfs/writeonly_file.go
  42. 16 13
      tsd/tsd.go
  43. 1 1
      tsnet/tsnet.go
  44. 1 1
      tstest/integration/tailscaled_deps_test_darwin.go
  45. 1 1
      tstest/integration/tailscaled_deps_test_freebsd.go
  46. 1 1
      tstest/integration/tailscaled_deps_test_linux.go
  47. 1 1
      tstest/integration/tailscaled_deps_test_openbsd.go
  48. 1 1
      tstest/integration/tailscaled_deps_test_windows.go
  49. 8 8
      wgengine/netstack/netstack.go
  50. 5 5
      wgengine/userspace.go

+ 10 - 10
client/tailscale/localclient.go

@@ -1418,25 +1418,25 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
 	return &cv, nil
 }
 
-// TailfsSetFileServerAddr instructs Tailfs to use the server at addr to access
+// 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 {
+// 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
+// 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 {
+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 {
+// 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",
@@ -1448,9 +1448,9 @@ func (lc *LocalClient) TailfsShareRemove(ctx context.Context, name string) error
 	return err
 }
 
-// TailfsShareList returns the list of shares that Tailfs is currently serving
+// 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) {
+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

+ 16 - 1
client/tailscale/localclient_test.go

@@ -5,7 +5,11 @@
 
 package tailscale
 
-import "testing"
+import (
+	"testing"
+
+	"tailscale.com/tstest/deptest"
+)
 
 func TestGetServeConfigFromJSON(t *testing.T) {
 	sc, err := getServeConfigFromJSON([]byte("null"))
@@ -25,3 +29,14 @@ func TestGetServeConfigFromJSON(t *testing.T) {
 		t.Errorf("want non-nil TCP for object")
 	}
 }
+
+func TestDeps(t *testing.T) {
+	deptest.DepChecker{
+		BadDeps: map[string]string{
+			// Make sure we don't again accidentally bring in a dependency on
+			// TailFS or its transitive dependencies
+			"tailscale.com/tailfs/tailfsimpl": "https://github.com/tailscale/tailscale/pull/10631",
+			"github.com/tailscale/gowebdav":   "https://github.com/tailscale/tailscale/pull/10631",
+		},
+	}.Check(t)
+}

+ 1 - 12
cmd/derper/depaware.txt

@@ -9,7 +9,6 @@ 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
@@ -20,7 +19,6 @@ 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
@@ -43,10 +41,7 @@ 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
@@ -115,13 +110,10 @@ 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+
@@ -188,7 +180,6 @@ 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/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+
@@ -205,7 +196,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
         cmp                                                          from slices+
         compress/flate                                               from compress/gzip+
         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/ecdh+
@@ -239,7 +229,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
         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 github.com/prometheus/client_golang/prometheus+
         flag                                                         from tailscale.com/cmd/derper+

+ 3 - 3
cmd/tailscale/cli/share.go

@@ -63,7 +63,7 @@ func runShareAdd(ctx context.Context, args []string) error {
 
 	name, path := args[0], args[1]
 
-	err := localClient.TailfsShareAdd(ctx, &tailfs.Share{
+	err := localClient.TailFSShareAdd(ctx, &tailfs.Share{
 		Name: name,
 		Path: path,
 	})
@@ -80,7 +80,7 @@ func runShareRemove(ctx context.Context, args []string) error {
 	}
 	name := args[0]
 
-	err := localClient.TailfsShareRemove(ctx, name)
+	err := localClient.TailFSShareRemove(ctx, name)
 	if err == nil {
 		fmt.Printf("Removed share %q\n", name)
 	}
@@ -93,7 +93,7 @@ func runShareList(ctx context.Context, args []string) error {
 		return fmt.Errorf("usage: tailscale %v", shareListUsage)
 	}
 
-	sharesMap, err := localClient.TailfsShareList(ctx)
+	sharesMap, err := localClient.TailFSShareList(ctx)
 	if err != nil {
 		return err
 	}

+ 3 - 13
cmd/tailscale/depaware.txt

@@ -9,7 +9,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
    L    github.com/coreos/go-systemd/v22/dbus                        from tailscale.com/clientupdate
    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
@@ -23,7 +22,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         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/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
@@ -53,11 +51,8 @@ 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
@@ -123,10 +118,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
      💣 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/tailfs                                         from tailscale.com/cmd/tailscale/cli+
         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+
@@ -205,7 +197,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         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 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+
@@ -224,7 +215,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         compress/flate                                               from compress/gzip+
         compress/gzip                                                from net/http+
         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/ecdh+
@@ -285,7 +275,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         math/big                                                     from crypto/dsa+
         math/bits                                                    from compress/flate+
         math/rand                                                    from github.com/mdlayher/netlink+
-        mime                                                         from github.com/tailscale/xnet/webdav+
+        mime                                                         from golang.org/x/oauth2/internal+
         mime/multipart                                               from net/http
         mime/quotedprintable                                         from mime/multipart
         net                                                          from crypto/tls+
@@ -306,7 +296,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         reflect                                                      from archive/tar+
         regexp                                                       from github.com/coreos/go-iptables/iptables+
         regexp/syntax                                                from regexp
-        runtime/debug                                                from golang.org/x/sync/singleflight+
+        runtime/debug                                                from nhooyr.io/websocket/internal/xsync+
         runtime/trace                                                from testing
         slices                                                       from tailscale.com/client/web+
         sort                                                         from archive/tar+

+ 8 - 7
cmd/tailscaled/depaware.txt

@@ -87,7 +87,7 @@ 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/djherbis/times                                    from tailscale.com/tailfs/tailfsimpl
         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
@@ -109,7 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    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
+        github.com/jellydator/ttlcache/v3                            from tailscale.com/tailfs/tailfsimpl/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+
@@ -155,7 +155,7 @@ 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/gowebdav                                from tailscale.com/tailfs/tailfsimpl/webdavfs
         github.com/tailscale/hujson                                  from tailscale.com/ipn/conffile
    L 💣 github.com/tailscale/netlink                                 from tailscale.com/net/routetable+
         github.com/tailscale/web-client-prebuilt                     from tailscale.com/client/web
@@ -169,7 +169,7 @@ 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                             from tailscale.com/tailfs/tailfsimpl+
         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
@@ -321,9 +321,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         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/tailfs/tailfsimpl                              from tailscale.com/cmd/tailscaled
+        tailscale.com/tailfs/tailfsimpl/compositefs                  from tailscale.com/tailfs/tailfsimpl
+        tailscale.com/tailfs/tailfsimpl/shared                       from tailscale.com/tailfs/tailfsimpl+
+        tailscale.com/tailfs/tailfsimpl/webdavfs                     from tailscale.com/tailfs/tailfsimpl
      💣 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

+ 15 - 13
cmd/tailscaled/tailscaled.go

@@ -52,7 +52,7 @@ import (
 	"tailscale.com/paths"
 	"tailscale.com/safesocket"
 	"tailscale.com/syncs"
-	"tailscale.com/tailfs"
+	"tailscale.com/tailfs/tailfsimpl"
 	"tailscale.com/tsd"
 	"tailscale.com/tsweb/varz"
 	"tailscale.com/types/flagtype"
@@ -141,7 +141,7 @@ var subCommands = map[string]func([]string) error{
 	"uninstall-system-daemon": uninstallSystemDaemon,
 	"debug":                   debugModeFunc,
 	"be-child":                beChild,
-	"serve-tailfs":            serveTailfs,
+	"serve-tailfs":            serveTailFS,
 }
 
 var beCLI func() // non-nil if CLI is linked in
@@ -403,6 +403,8 @@ func run() (err error) {
 		debugMux = newDebugMux()
 	}
 
+	sys.Set(tailfsimpl.NewFileSystemForRemote(logf))
+
 	return startIPNServer(context.Background(), logf, pol.PublicID, sys)
 }
 
@@ -625,12 +627,12 @@ var tstunNew = tstun.New
 
 func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
 	conf := wgengine.Config{
-		ListenPort:   args.port,
-		NetMon:       sys.NetMon.Get(),
-		Dialer:       sys.Dialer.Get(),
-		SetSubsystem: sys.Set,
-		ControlKnobs: sys.ControlKnobs(),
-		EnableTailfs: true,
+		ListenPort:     args.port,
+		NetMon:         sys.NetMon.Get(),
+		Dialer:         sys.Dialer.Get(),
+		SetSubsystem:   sys.Set,
+		ControlKnobs:   sys.ControlKnobs(),
+		TailFSForLocal: tailfsimpl.NewFileSystemForLocal(logf),
 	}
 
 	onlyNetstack = name == "userspace-networking"
@@ -733,7 +735,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
 }
 
 func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
-	tfs, _ := sys.TailfsForLocal.GetOK()
+	tfs, _ := sys.TailFSForLocal.GetOK()
 	ret, err := netstack.Create(logf,
 		sys.Tun.Get(),
 		sys.Engine.Get(),
@@ -809,21 +811,21 @@ func beChild(args []string) error {
 	return f(args[1:])
 }
 
-// serveTailfs serves one or more tailfs on localhost using the WebDAV
+// 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
+// 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 {
+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()
+	s, err := tailfsimpl.NewFileServer()
 	if err != nil {
 		return fmt.Errorf("unable to start tailfs FileServer: %v", err)
 	}

+ 7 - 6
ipn/backend.go

@@ -66,7 +66,7 @@ const (
 	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
-	NotifyInitialTailfsShares // if set, the first Notify message (sent immediately) will contain the current Tailfs Shares
+	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
@@ -122,11 +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"`
+	// TailFSShares tracks the 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
 }

+ 24 - 24
ipn/ipnlocal/local.go

@@ -67,7 +67,6 @@ import (
 	"tailscale.com/syncs"
 	"tailscale.com/tailcfg"
 	"tailscale.com/taildrop"
-	"tailscale.com/tailfs"
 	"tailscale.com/tka"
 	"tailscale.com/tsd"
 	"tailscale.com/tstime"
@@ -288,8 +287,7 @@ 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
+	tailFSListeners map[netip.AddrPort]*localListener // listeners for local tailfs traffic
 
 	// statusLock must be held before calling statusChanged.Wait() or
 	// statusChanged.Broadcast().
@@ -432,13 +430,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)
+	// initialize TailFS shares from saved state
+	fs, ok := b.sys.TailFSForRemote.GetOK()
+	if !ok {
+		b.mu.Lock()
+		shares, err := b.tailFSGetSharesLocked()
+		b.mu.Unlock()
+		if err == nil && len(shares) > 0 {
+			fs.SetShares(shares)
+		}
 	}
 
 	return b, nil
@@ -2268,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 | ipn.NotifyInitialTailfsShares
+	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 {
@@ -2284,14 +2284,14 @@ 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 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))
+				ini.TailFSShares = make(map[string]string, len(shares))
 				for _, share := range shares {
-					ini.TailfsShares[share.Name] = share.Path
+					ini.TailFSShares[share.Name] = share.Path
 				}
 			}
 		}
@@ -3337,8 +3337,8 @@ 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 dst.Port() == TailFSLocalPort {
+		fs, ok := b.sys.TailFSForLocal.GetOK()
 		if ok {
 			return func(conn net.Conn) error {
 				return fs.HandleConn(conn, conn.RemoteAddr())
@@ -4642,9 +4642,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
 		}
 	}
 
-	if b.tailfsSharingEnabledLocked() {
-		b.updateTailfsPeersLocked(nm)
-		b.tailfsNotifyCurrentSharesLocked()
+	if b.tailFSSharingEnabledLocked() {
+		b.updateTailFSPeersLocked(nm)
+		b.tailFSNotifyCurrentSharesLocked()
 	}
 }
 
@@ -4672,14 +4672,14 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
 	}
 }
 
-// tailfsTransport is an http.RoundTripper that uses the latest value of
+// 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 {
+type tailFSTransport struct {
 	b *LocalBackend
 }
 
-func (t *tailfsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+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
@@ -4767,7 +4767,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
 	}
 
 	if !b.sys.IsNetstack() {
-		b.updateTailfsListenersLocked()
+		b.updateTailFSListenersLocked()
 	}
 
 	b.reloadServeConfigLocked(prefs)

+ 9 - 11
ipn/ipnlocal/peerapi.go

@@ -46,7 +46,7 @@ import (
 )
 
 const (
-	tailfsPrefix = "/v0/tailfs"
+	tailFSPrefix = "/v0/tailfs"
 )
 
 var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, string) error
@@ -322,8 +322,8 @@ 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)
+	if strings.HasPrefix(r.URL.Path, tailFSPrefix) {
+		h.handleServeTailFS(w, r)
 		return
 	}
 	switch r.URL.Path {
@@ -1103,14 +1103,14 @@ 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() {
+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]
+	tailfsCaps, ok := capsMap[tailcfg.PeerCapabilityTailFS]
 	if !ok {
 		http.Error(w, "tailfs not permitted", http.StatusForbidden)
 		return
@@ -1127,14 +1127,12 @@ func (h *peerAPIHandler) handleServeTailfs(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	h.ps.b.mu.Lock()
-	fs := h.ps.b.tailfsForRemote
-	h.ps.b.mu.Unlock()
-	if fs == nil {
+	fs, ok := h.ps.b.sys.TailFSForRemote.GetOK()
+	if !ok {
 		http.Error(w, "tailfs not enabled", http.StatusNotFound)
 		return
 	}
-	r.URL.Path = strings.TrimPrefix(r.URL.Path, tailfsPrefix)
+	r.URL.Path = strings.TrimPrefix(r.URL.Path, tailFSPrefix)
 	fs.ServeHTTPWithPerms(p, w, r)
 }
 

+ 52 - 47
ipn/ipnlocal/tailfs.go

@@ -24,9 +24,9 @@ import (
 )
 
 const (
-	// TailfsLocalPort is the port on which the Tailfs listens for location
+	// TailFSLocalPort is the port on which the TailFS listens for location
 	// connections on quad 100.
-	TailfsLocalPort = 8080
+	TailFSLocalPort = 8080
 
 	tailfsSharesStateKey = ipn.StateKey("_tailfs-shares")
 )
@@ -36,27 +36,25 @@ var (
 	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
+// 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 {
+func (b *LocalBackend) TailFSSharingEnabled() bool {
 	b.mu.Lock()
 	defer b.mu.Unlock()
-	return b.tailfsSharingEnabledLocked()
+	return b.tailFSSharingEnabledLocked()
 }
 
-func (b *LocalBackend) tailfsSharingEnabledLocked() bool {
-	return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailfsSharingEnabled)
+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
+// 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 {
+func (b *LocalBackend) TailFSSetFileServerAddr(addr string) error {
+	fs, ok := b.sys.TailFSForRemote.GetOK()
+	if !ok {
 		return errors.New("tailfs not enabled")
 	}
 
@@ -64,11 +62,11 @@ func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error {
 	return nil
 }
 
-// TailfsAddShare adds the given share if no share with that name exists, or
+// 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 {
+func (b *LocalBackend) TailFSAddShare(share *tailfs.Share) error {
 	var err error
 	share.Name, err = normalizeShareName(share.Name)
 	if err != nil {
@@ -104,11 +102,12 @@ func normalizeShareName(name string) (string, error) {
 }
 
 func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]string, error) {
-	if b.tailfsForRemote == nil {
+	fs, ok := b.sys.TailFSForRemote.GetOK()
+	if !ok {
 		return nil, errors.New("tailfs not enabled")
 	}
 
-	shares, err := b.tailfsGetSharesLocked()
+	shares, err := b.tailFSGetSharesLocked()
 	if err != nil {
 		return nil, err
 	}
@@ -121,17 +120,21 @@ func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]str
 	if err != nil {
 		return nil, fmt.Errorf("write state: %w", err)
 	}
-	b.tailfsForRemote.SetShares(shares)
+	fs.SetShares(shares)
 
 	return shareNameMap(shares), nil
 }
 
-// TailfsRemoveShare removes the named share. Share names are forced to
+// TailFSRemoveShare removes the named share. Share names are forced to
 // lowercase.
-func (b *LocalBackend) TailfsRemoveShare(name string) error {
+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)
+	var err error
+	name, err = normalizeShareName(name)
+	if err != nil {
+		return err
+	}
 
 	b.mu.Lock()
 	shares, err := b.tailfsRemoveShareLocked(name)
@@ -145,11 +148,12 @@ func (b *LocalBackend) TailfsRemoveShare(name string) error {
 }
 
 func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string, error) {
-	if b.tailfsForRemote == nil {
+	fs, ok := b.sys.TailFSForRemote.GetOK()
+	if !ok {
 		return nil, errors.New("tailfs not enabled")
 	}
 
-	shares, err := b.tailfsGetSharesLocked()
+	shares, err := b.tailFSGetSharesLocked()
 	if err != nil {
 		return nil, err
 	}
@@ -166,7 +170,7 @@ func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string,
 	if err != nil {
 		return nil, fmt.Errorf("write state: %w", err)
 	}
-	b.tailfsForRemote.SetShares(shares)
+	fs.SetShares(shares)
 
 	return shareNameMap(shares), nil
 }
@@ -182,13 +186,13 @@ func shareNameMap(sharesByName map[string]*tailfs.Share) map[string]string {
 // 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})
+	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()
+// 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
@@ -197,15 +201,16 @@ func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() {
 	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) {
+// TailFSGetShares returns the current set of shares from the state store,
+// stored under ipn.StateKey("_tailfs-shares").
+func (b *LocalBackend) TailFSGetShares() (map[string]*tailfs.Share, error) {
 	b.mu.Lock()
 	defer b.mu.Unlock()
 
-	return b.tailfsGetSharesLocked()
+	return b.tailFSGetSharesLocked()
 }
 
-func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error) {
+func (b *LocalBackend) tailFSGetSharesLocked() (map[string]*tailfs.Share, error) {
 	data, err := b.store.ReadState(tailfsSharesStateKey)
 	if err != nil {
 		if errors.Is(err, ipn.ErrStateNotExist) {
@@ -223,27 +228,27 @@ func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error)
 	return shares, nil
 }
 
-// updateTailfsListenersLocked creates listeners on the local Tailfs port.
+// 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() {
+func (b *LocalBackend) updateTailFSListenersLocked() {
 	if b.netMap == nil {
 		return
 	}
 
 	addrs := b.netMap.GetAddresses()
-	oldListeners := b.tailfsListeners
+	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 {
+		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)
+			sl := b.newTailFSListener(context.Background(), fs, addrPort, b.logf)
 			newListeners[addrPort] = sl
 			go sl.Run()
 		}
@@ -255,9 +260,9 @@ func (b *LocalBackend) updateTailfsListenersLocked() {
 	}
 }
 
-// newTailfsListener returns a listener for local connections to a tailfs
+// 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 {
+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,
@@ -273,10 +278,10 @@ func (b *LocalBackend) newTailfsListener(ctx context.Context, fs *tailfs.FileSys
 	}
 }
 
-// updateTailfsPeersLocked sets all applicable peers from the netmap as tailfs
+// updateTailFSPeersLocked sets all applicable peers from the netmap as tailfs
 // remotes.
-func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
-	fs, ok := b.sys.TailfsForLocal.GetOK()
+func (b *LocalBackend) updateTailFSPeersLocked(nm *netmap.NetworkMap) {
+	fs, ok := b.sys.TailFSForLocal.GetOK()
 	if !ok {
 		return
 	}
@@ -284,7 +289,7 @@ func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
 	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:])
+		url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailFSPrefix[1:])
 		tailfsRemotes = append(tailfsRemotes, &tailfs.Remote{
 			Name: p.DisplayName(false),
 			URL:  url,
@@ -314,5 +319,5 @@ func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
 			},
 		})
 	}
-	fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailfsTransport{b: b})
+	fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailFSTransport{b: b})
 }

+ 8 - 8
ipn/localapi/localapi.go

@@ -110,7 +110,7 @@ var handler = map[string]localAPIHandler{
 	"serve-config":                (*Handler).serveServeConfig,
 	"set-dns":                     (*Handler).serveSetDNS,
 	"set-expiry-sooner":           (*Handler).serveSetExpirySooner,
-	"tailfs/fileserver-address":   (*Handler).serveTailfsFileServerAddr,
+	"tailfs/fileserver-address":   (*Handler).serveTailFSFileServerAddr,
 	"tailfs/shares":               (*Handler).serveShares,
 	"start":                       (*Handler).serveStart,
 	"status":                      (*Handler).serveStatus,
@@ -2531,8 +2531,8 @@ 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) {
+// 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
@@ -2544,13 +2544,13 @@ func (h *Handler) serveTailfsFileServerAddr(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
-	h.b.TailfsSetFileServerAddr(string(b))
+	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() {
+	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
 	}
@@ -2581,7 +2581,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
 			}
 			share.As = username
 		}
-		err = h.b.TailfsAddShare(&share)
+		err = h.b.TailFSAddShare(&share)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -2594,7 +2594,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			return
 		}
-		err = h.b.TailfsRemoveShare(share.Name)
+		err = h.b.TailFSRemoveShare(share.Name)
 		if err != nil {
 			if os.IsNotExist(err) {
 				http.Error(w, "share not found", http.StatusNotFound)
@@ -2605,7 +2605,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
 		}
 		w.WriteHeader(http.StatusNoContent)
 	case "GET":
-		shares, err := h.b.TailfsGetShares()
+		shares, err := h.b.TailFSGetShares()
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return

+ 4 - 4
tailcfg/tailcfg.go

@@ -1345,8 +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"
+	// 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
@@ -2211,8 +2211,8 @@ const (
 	// tail end of an active direct connection in magicsock.
 	NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime"
 
-	// NodeAttrsTailfsSharingEnabled enables sharing via Tailfs.
-	NodeAttrsTailfsSharingEnabled NodeCapability = "tailfs:share"
+	// NodeAttrsTailFSSharingEnabled enables sharing via TailFS.
+	NodeAttrsTailFSSharingEnabled NodeCapability = "tailfs:share"
 )
 
 // SetDNSRequest is a request to add a DNS record.

+ 12 - 0
tailcfg/tailcfg_test.go

@@ -15,6 +15,7 @@ import (
 	"time"
 
 	. "tailscale.com/tailcfg"
+	"tailscale.com/tstest/deptest"
 	"tailscale.com/types/key"
 	"tailscale.com/types/opt"
 	"tailscale.com/types/ptr"
@@ -842,3 +843,14 @@ func TestRawMessage(t *testing.T) {
 		})
 	}
 }
+
+func TestDeps(t *testing.T) {
+	deptest.DepChecker{
+		BadDeps: map[string]string{
+			// Make sure we don't again accidentally bring in a dependency on
+			// TailFS or its transitive dependencies
+			"tailscale.com/tailfs/tailfsimpl": "https://github.com/tailscale/tailscale/pull/10631",
+			"github.com/tailscale/gowebdav":   "https://github.com/tailscale/tailscale/pull/10631",
+		},
+	}.Check(t)
+}

+ 18 - 80
tailfs/local.go

@@ -1,99 +1,37 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
+// Package tailfs provides a filesystem that allows sharing folders between
+// Tailscale nodes using WebDAV. The actual implementation of the core TailFS
+// functionality lives in package tailfsimpl. These packages are separated to
+// allow users of tailfs to refer to the interfaces without having a hard
+// dependency on tailfs, so that programs which don't actually use tailfs can
+// avoid its transitive dependencies.
 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.
+// 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)
-}
+// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
+// provides a unified WebDAV interface to remote TailFS shares on other nodes.
+type FileSystemForLocal interface {
+	// HandleConn handles connections from local WebDAV clients
+	HandleConn(conn net.Conn, remoteAddr net.Addr) error
 
-// 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...)
-}
+	// 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.
+	SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper)
 
-// Close() stops serving the WebDAV content
-func (s *FileSystemForLocal) Close() error {
-	s.cfs.Close()
-	return s.listener.Close()
+	// Close() stops serving the WebDAV content
+	Close() error
 }

+ 31 - 360
tailfs/remote.go

@@ -4,386 +4,57 @@
 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
+	// DisallowShareAs forcibly disables sharing as a specific user, only used
+	// for testing.
+	DisallowShareAs = false
 )
 
 // AllowShareAs reports whether sharing files as a specific user is allowed.
 func AllowShareAs() bool {
-	return !disallowShareAs && doAllowShareAs()
+	return !DisallowShareAs && doAllowShareAs()
 }
 
-// Share represents a folder that's shared with remote Tailfs nodes.
+// Share configures a folder to be shared through TailFS.
 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.
+	// Can be left blank to use the default value of "whoever is running the
+	// Tailscale GUI".
 	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
+// 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
-	s.userServers = userServers
-	s.mu.Unlock()
-
-	s.stopUserServers(oldUserServers)
-	s.closeFileSystems(oldFileSystems)
-}
-
-func (s *FileSystemForRemote) buildWebDAVFS(share *Share) webdav.FileSystem {
-	return webdavfs.New(webdavfs.Options{
-		Logf: s.logf,
-		URL:  fmt.Sprintf("http://%v/%v", hex.EncodeToString([]byte(share.Name)), share.Name),
-		Transport: &http.Transport{
-			Dial: func(_, shareAddr string) (net.Conn, error) {
-				shareNameHex, _, err := net.SplitHostPort(shareAddr)
-				if err != nil {
-					return nil, fmt.Errorf("unable to parse share address %v: %w", shareAddr, err)
-				}
-
-				// We had to encode the share name in hex to make sure it's a valid hostname
-				shareNameBytes, err := hex.DecodeString(shareNameHex)
-				if err != nil {
-					return nil, fmt.Errorf("unable to decode share name from host %v: %v", shareNameHex, err)
-				}
-				shareName := string(shareNameBytes)
-
-				s.mu.RLock()
-				share, shareFound := s.shares[shareName]
-				userServers := s.userServers
-				fileServerAddr := s.fileServerAddr
-				s.mu.RUnlock()
-
-				if !shareFound {
-					return nil, fmt.Errorf("unknown share %v", shareName)
-				}
-
-				var addr string
-				if !AllowShareAs() {
-					addr = fileServerAddr
-				} else {
-					userServer, found := userServers[share.As]
-					if found {
-						userServer.mu.RLock()
-						addr = userServer.addr
-						userServer.mu.RUnlock()
-					}
-				}
-
-				if addr == "" {
-					return nil, fmt.Errorf("unable to determine address for share %v", shareName)
-				}
-
-				_, err = netip.ParseAddrPort(addr)
-				if err == nil {
-					// this is a regular network address, dial normally
-					return net.Dial("tcp", addr)
-				}
-				// assume this is a safesocket address
-				return safesocket.Connect(addr)
-			},
-		},
-		StatRoot: true,
-	})
-}
-
-// ServeHTTPWithPerms behaves like the similar method from http.Handler but
-// also accepts a Permissions map that captures the permissions of the
-// connecting node.
-func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request) {
-	isWrite := writeMethods[r.Method]
-	if isWrite {
-		share := shared.CleanAndSplit(r.URL.Path)[0]
-		switch permissions.For(share) {
-		case PermissionNone:
-			// If we have no permissions to this share, treat it as not found
-			// to avoid leaking any information about the share's existence.
-			http.Error(w, "not found", http.StatusNotFound)
-			return
-		case PermissionReadOnly:
-			http.Error(w, "permission denied", http.StatusForbidden)
-			return
-		}
-	}
-
-	s.mu.RLock()
-	fileSystems := s.fileSystems
-	s.mu.RUnlock()
-
-	children := make([]*compositefs.Child, 0, len(fileSystems))
-	// filter out shares to which the connecting principal has no access
-	for name, fs := range fileSystems {
-		if permissions.For(name) == PermissionNone {
-			continue
-		}
-
-		children = append(children, &compositefs.Child{Name: name, FS: fs})
-	}
-
-	cfs := compositefs.New(
-		compositefs.Options{
-			Logf:         s.logf,
-			StatChildren: true,
-		})
-	cfs.SetChildren(children...)
-	h := webdav.Handler{
-		FileSystem: cfs,
-		LockSystem: s.lockSystem,
-	}
-	h.ServeHTTP(w, r)
-}
-
-func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
-	for _, server := range userServers {
-		if err := server.Close(); err != nil {
-			s.logf("error closing tailfs user server: %v", err)
-		}
-	}
-}
-
-func (s *FileSystemForRemote) closeFileSystems(fileSystems map[string]webdav.FileSystem) {
-	for _, fs := range fileSystems {
-		closer, ok := fs.(interface{ Close() error })
-		if ok {
-			if err := closer.Close(); err != nil {
-				s.logf("error closing tailfs filesystem: %v", err)
-			}
-		}
-	}
-}
-
-// Close() stops serving the WebDAV content
-func (s *FileSystemForRemote) Close() error {
-	s.mu.Lock()
-	userServers := s.userServers
-	fileSystems := s.fileSystems
-	s.mu.Unlock()
-
-	s.stopUserServers(userServers)
-	s.closeFileSystems(fileSystems)
-	return nil
-}
-
-// userServer runs tailscaled serve-tailfs to serve webdav content for the
-// given Shares. All Shares are assumed to have the same Share.As, and the
-// content is served as that Share.As user.
-type userServer struct {
-	logf   logger.Logf
-	shares []*Share
-
-	// 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
-	cmd    *exec.Cmd
-	addr   string
-	closed bool
-}
-
-func (s *userServer) Close() error {
-	s.mu.Lock()
-	cmd := s.cmd
-	s.closed = true
-	s.mu.Unlock()
-	if cmd != nil && cmd.Process != nil {
-		return cmd.Process.Kill()
-	}
-	// not running, that's okay
-	return nil
-}
-
-func (s *userServer) runLoop() {
-	executable, err := os.Executable()
-	if err != nil {
-		s.logf("can't find executable: %v", err)
-		return
-	}
-	maxSleepTime := 30 * time.Second
-	consecutiveFailures := float64(0)
-	var timeOfLastFailure time.Time
-	for {
-		s.mu.RLock()
-		closed := s.closed
-		s.mu.RUnlock()
-		if closed {
-			return
-		}
-
-		err := s.run(executable)
-		now := time.Now()
-		timeSinceLastFailure := now.Sub(timeOfLastFailure)
-		timeOfLastFailure = now
-		if timeSinceLastFailure < maxSleepTime {
-			consecutiveFailures++
-		} else {
-			consecutiveFailures = 1
-		}
-		sleepTime := time.Duration(math.Pow(2, consecutiveFailures)) * time.Millisecond
-		if sleepTime > maxSleepTime {
-			sleepTime = maxSleepTime
-		}
-		s.logf("user server % v stopped with error %v, will try again in %v", executable, err, sleepTime)
-		time.Sleep(sleepTime)
-	}
-}
-
-// Run runs the executable (tailscaled). This function only works on UNIX systems,
-// but those are the only ones on which we use userServers anyway.
-func (s *userServer) run(executable string) error {
-	// set up the command
-	args := []string{"serve-tailfs"}
-	for _, s := range s.shares {
-		args = append(args, s.Name, s.Path)
-	}
-	allArgs := []string{"-u", s.shares[0].As, executable}
-	allArgs = append(allArgs, args...)
-	cmd := exec.Command("sudo", allArgs...)
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		return fmt.Errorf("stdout pipe: %w", err)
-	}
-	defer stdout.Close()
-	stderr, err := cmd.StderrPipe()
-	if err != nil {
-		return fmt.Errorf("stderr pipe: %w", err)
-	}
-	defer stderr.Close()
-
-	err = cmd.Start()
-	if err != nil {
-		return fmt.Errorf("start: %w", err)
-	}
-	s.mu.Lock()
-	s.cmd = cmd
-	s.mu.Unlock()
-
-	// read address
-	stdoutScanner := bufio.NewScanner(stdout)
-	stdoutScanner.Scan()
-	if stdoutScanner.Err() != nil {
-		return fmt.Errorf("read addr: %w", stdoutScanner.Err())
-	}
-	addr := stdoutScanner.Text()
-	// send the rest of stdout and stderr to logger to avoid blocking
-	go func() {
-		for stdoutScanner.Scan() {
-			s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
-		}
-	}()
-	stderrScanner := bufio.NewScanner(stderr)
-	go func() {
-		for stderrScanner.Scan() {
-			s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
-		}
-	}()
-	s.mu.Lock()
-	s.addr = strings.TrimSpace(addr)
-	s.mu.Unlock()
-	return cmd.Wait()
-}
-
-var writeMethods = map[string]bool{
-	"PUT":       true,
-	"POST":      true,
-	"COPY":      true,
-	"LOCK":      true,
-	"UNLOCK":    true,
-	"MKCOL":     true,
-	"MOVE":      true,
-	"PROPPATCH": true,
+type FileSystemForRemote interface {
+	// 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.
+	SetFileServerAddr(addr string)
+
+	// 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.
+	SetShares(shares map[string]*Share)
+
+	// ServeHTTPWithPerms behaves like the similar method from http.Handler but
+	// also accepts a Permissions map that captures the permissions of the
+	// connecting node.
+	ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request)
+
+	// Close() stops serving the WebDAV content
+	Close() error
 }

+ 0 - 18
tailfs/tailfs.go

@@ -1,18 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package tailfs provides a filesystem that allows sharing folders between
-// Tailscale nodes using WebDAV.
-package tailfs
-
-import (
-	"time"
-)
-
-const (
-	// statCacheTTL causes the local WebDAV proxy to cache file metadata to
-	// avoid excessive network roundtrips. This is similar to the
-	// DirectoryCacheLifetime setting of Windows' built-in SMB client,
-	// see https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-7/ff686200(v=ws.10)
-	statCacheTTL = 10 * time.Second
-)

+ 1 - 1
tailfs/birthtiming.go → tailfs/tailfsimpl/birthtiming.go

@@ -1,7 +1,7 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
-package tailfs
+package tailfsimpl
 
 import (
 	"context"

+ 1 - 1
tailfs/birthtiming_test.go → tailfs/tailfsimpl/birthtiming_test.go

@@ -5,7 +5,7 @@
 
 //go:build windows || darwin
 
-package tailfs
+package tailfsimpl
 
 import (
 	"context"

+ 1 - 1
tailfs/compositefs/compositefs.go → tailfs/tailfsimpl/compositefs/compositefs.go

@@ -15,7 +15,7 @@ import (
 	"time"
 
 	"github.com/tailscale/xnet/webdav"
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 	"tailscale.com/tstime"
 	"tailscale.com/types/logger"
 )

+ 1 - 1
tailfs/compositefs/compositefs_test.go → tailfs/tailfsimpl/compositefs/compositefs_test.go

@@ -15,7 +15,7 @@ import (
 	"time"
 
 	"github.com/tailscale/xnet/webdav"
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 	"tailscale.com/tstest"
 )
 

+ 1 - 1
tailfs/compositefs/mkdir.go → tailfs/tailfsimpl/compositefs/mkdir.go

@@ -7,7 +7,7 @@ import (
 	"context"
 	"os"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 )
 
 // Mkdir implements webdav.Filesystem. The root of this file system is

+ 1 - 1
tailfs/compositefs/openfile.go → tailfs/tailfsimpl/compositefs/openfile.go

@@ -9,7 +9,7 @@ import (
 	"os"
 
 	"github.com/tailscale/xnet/webdav"
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 )
 
 // OpenFile implements interface webdav.Filesystem.

+ 1 - 1
tailfs/compositefs/removeall.go → tailfs/tailfsimpl/compositefs/removeall.go

@@ -7,7 +7,7 @@ import (
 	"context"
 	"os"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 )
 
 // RemoveAll implements webdav.File. The root of this file system is read-only,

+ 1 - 1
tailfs/compositefs/rename.go → tailfs/tailfsimpl/compositefs/rename.go

@@ -7,7 +7,7 @@ import (
 	"context"
 	"os"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 )
 
 // Rename implements interface webdav.FileSystem. The root of this file system

+ 1 - 1
tailfs/compositefs/stat.go → tailfs/tailfsimpl/compositefs/stat.go

@@ -7,7 +7,7 @@ import (
 	"context"
 	"io/fs"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 )
 
 // Stat implements webdav.FileSystem.

+ 1 - 1
tailfs/connlistener.go → tailfs/tailfsimpl/connlistener.go

@@ -1,7 +1,7 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
-package tailfs
+package tailfsimpl
 
 import (
 	"log"

+ 1 - 1
tailfs/connlistener_test.go → tailfs/tailfsimpl/connlistener_test.go

@@ -1,7 +1,7 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
-package tailfs
+package tailfsimpl
 
 import (
 	"log"

+ 3 - 3
tailfs/fileserver.go → tailfs/tailfsimpl/fileserver.go

@@ -1,7 +1,7 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
-package tailfs
+package tailfsimpl
 
 import (
 	"net"
@@ -9,11 +9,11 @@ import (
 	"sync"
 
 	"github.com/tailscale/xnet/webdav"
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/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
+// 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

+ 103 - 0
tailfs/tailfsimpl/local_impl.go

@@ -0,0 +1,103 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package tailfsimpl provides an implementation of package tailfs.
+package tailfsimpl
+
+import (
+	"log"
+	"net"
+	"net/http"
+	"time"
+
+	"github.com/tailscale/xnet/webdav"
+	"tailscale.com/tailfs"
+	"tailscale.com/tailfs/tailfsimpl/compositefs"
+	"tailscale.com/tailfs/tailfsimpl/webdavfs"
+	"tailscale.com/types/logger"
+)
+
+const (
+	// statCacheTTL causes the local WebDAV proxy to cache file metadata to
+	// avoid excessive network roundtrips. This is similar to the
+	// DirectoryCacheLifetime setting of Windows' built-in SMB client,
+	// see https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-7/ff686200(v=ws.10)
+	statCacheTTL = 10 * time.Second
+)
+
+// 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 []*tailfs.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()
+}

+ 359 - 0
tailfs/tailfsimpl/remote_impl.go

@@ -0,0 +1,359 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package tailfsimpl
+
+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"
+	"tailscale.com/tailfs/tailfsimpl/compositefs"
+	"tailscale.com/tailfs/tailfsimpl/shared"
+	"tailscale.com/tailfs/tailfsimpl/webdavfs"
+	"tailscale.com/types/logger"
+)
+
+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 implements tailfs.FileSystemForRemote.
+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]*tailfs.Share
+	fileSystems    map[string]webdav.FileSystem
+	userServers    map[string]*userServer
+}
+
+// SetFileServerAddr implements tailfs.FileSystemForRemote.
+func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
+	s.mu.Lock()
+	s.fileServerAddr = addr
+	s.mu.Unlock()
+}
+
+// SetShares implements tailfs.FileSystemForRemote.
+func (s *FileSystemForRemote) SetShares(shares map[string]*tailfs.Share) {
+	userServers := make(map[string]*userServer)
+	if tailfs.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
+	s.userServers = userServers
+	s.mu.Unlock()
+
+	s.stopUserServers(oldUserServers)
+	s.closeFileSystems(oldFileSystems)
+}
+
+func (s *FileSystemForRemote) buildWebDAVFS(share *tailfs.Share) webdav.FileSystem {
+	return webdavfs.New(webdavfs.Options{
+		Logf: s.logf,
+		URL:  fmt.Sprintf("http://%v/%v", hex.EncodeToString([]byte(share.Name)), share.Name),
+		Transport: &http.Transport{
+			Dial: func(_, shareAddr string) (net.Conn, error) {
+				shareNameHex, _, err := net.SplitHostPort(shareAddr)
+				if err != nil {
+					return nil, fmt.Errorf("unable to parse share address %v: %w", shareAddr, err)
+				}
+
+				// We had to encode the share name in hex to make sure it's a valid hostname
+				shareNameBytes, err := hex.DecodeString(shareNameHex)
+				if err != nil {
+					return nil, fmt.Errorf("unable to decode share name from host %v: %v", shareNameHex, err)
+				}
+				shareName := string(shareNameBytes)
+
+				s.mu.RLock()
+				share, shareFound := s.shares[shareName]
+				userServers := s.userServers
+				fileServerAddr := s.fileServerAddr
+				s.mu.RUnlock()
+
+				if !shareFound {
+					return nil, fmt.Errorf("unknown share %v", shareName)
+				}
+
+				var addr string
+				if !tailfs.AllowShareAs() {
+					addr = fileServerAddr
+				} else {
+					userServer, found := userServers[share.As]
+					if found {
+						userServer.mu.RLock()
+						addr = userServer.addr
+						userServer.mu.RUnlock()
+					}
+				}
+
+				if addr == "" {
+					return nil, fmt.Errorf("unable to determine address for share %v", shareName)
+				}
+
+				_, err = netip.ParseAddrPort(addr)
+				if err == nil {
+					// this is a regular network address, dial normally
+					return net.Dial("tcp", addr)
+				}
+				// assume this is a safesocket address
+				return safesocket.Connect(addr)
+			},
+		},
+		StatRoot: true,
+	})
+}
+
+// ServeHTTPWithPerms implements tailfs.FileSystemForRemote.
+func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions, w http.ResponseWriter, r *http.Request) {
+	isWrite := writeMethods[r.Method]
+	if isWrite {
+		share := shared.CleanAndSplit(r.URL.Path)[0]
+		switch permissions.For(share) {
+		case tailfs.PermissionNone:
+			// If we have no permissions to this share, treat it as not found
+			// to avoid leaking any information about the share's existence.
+			http.Error(w, "not found", http.StatusNotFound)
+			return
+		case tailfs.PermissionReadOnly:
+			http.Error(w, "permission denied", http.StatusForbidden)
+			return
+		}
+	}
+
+	s.mu.RLock()
+	fileSystems := s.fileSystems
+	s.mu.RUnlock()
+
+	children := make([]*compositefs.Child, 0, len(fileSystems))
+	// filter out shares to which the connecting principal has no access
+	for name, fs := range fileSystems {
+		if permissions.For(name) == tailfs.PermissionNone {
+			continue
+		}
+
+		children = append(children, &compositefs.Child{Name: name, FS: fs})
+	}
+
+	cfs := compositefs.New(
+		compositefs.Options{
+			Logf:         s.logf,
+			StatChildren: true,
+		})
+	cfs.SetChildren(children...)
+	h := webdav.Handler{
+		FileSystem: cfs,
+		LockSystem: s.lockSystem,
+	}
+	h.ServeHTTP(w, r)
+}
+
+func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
+	for _, server := range userServers {
+		if err := server.Close(); err != nil {
+			s.logf("error closing tailfs user server: %v", err)
+		}
+	}
+}
+
+func (s *FileSystemForRemote) closeFileSystems(fileSystems map[string]webdav.FileSystem) {
+	for _, fs := range fileSystems {
+		closer, ok := fs.(interface{ Close() error })
+		if ok {
+			if err := closer.Close(); err != nil {
+				s.logf("error closing tailfs filesystem: %v", err)
+			}
+		}
+	}
+}
+
+// Close() implements tailfs.FileSystemForRemote.
+func (s *FileSystemForRemote) Close() error {
+	s.mu.Lock()
+	userServers := s.userServers
+	fileSystems := s.fileSystems
+	s.mu.Unlock()
+
+	s.stopUserServers(userServers)
+	s.closeFileSystems(fileSystems)
+	return nil
+}
+
+// userServer runs tailscaled serve-tailfs to serve webdav content for the
+// given Shares. All Shares are assumed to have the same Share.As, and the
+// content is served as that Share.As user.
+type userServer struct {
+	logf   logger.Logf
+	shares []*tailfs.Share
+
+	// 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
+	cmd    *exec.Cmd
+	addr   string
+	closed bool
+}
+
+func (s *userServer) Close() error {
+	s.mu.Lock()
+	cmd := s.cmd
+	s.closed = true
+	s.mu.Unlock()
+	if cmd != nil && cmd.Process != nil {
+		return cmd.Process.Kill()
+	}
+	// not running, that's okay
+	return nil
+}
+
+func (s *userServer) runLoop() {
+	executable, err := os.Executable()
+	if err != nil {
+		s.logf("can't find executable: %v", err)
+		return
+	}
+	maxSleepTime := 30 * time.Second
+	consecutiveFailures := float64(0)
+	var timeOfLastFailure time.Time
+	for {
+		s.mu.RLock()
+		closed := s.closed
+		s.mu.RUnlock()
+		if closed {
+			return
+		}
+
+		err := s.run(executable)
+		now := time.Now()
+		timeSinceLastFailure := now.Sub(timeOfLastFailure)
+		timeOfLastFailure = now
+		if timeSinceLastFailure < maxSleepTime {
+			consecutiveFailures++
+		} else {
+			consecutiveFailures = 1
+		}
+		sleepTime := time.Duration(math.Pow(2, consecutiveFailures)) * time.Millisecond
+		if sleepTime > maxSleepTime {
+			sleepTime = maxSleepTime
+		}
+		s.logf("user server % v stopped with error %v, will try again in %v", executable, err, sleepTime)
+		time.Sleep(sleepTime)
+	}
+}
+
+// Run runs the executable (tailscaled). This function only works on UNIX systems,
+// but those are the only ones on which we use userServers anyway.
+func (s *userServer) run(executable string) error {
+	// set up the command
+	args := []string{"serve-tailfs"}
+	for _, s := range s.shares {
+		args = append(args, s.Name, s.Path)
+	}
+	allArgs := []string{"-u", s.shares[0].As, executable}
+	allArgs = append(allArgs, args...)
+	cmd := exec.Command("sudo", allArgs...)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return fmt.Errorf("stdout pipe: %w", err)
+	}
+	defer stdout.Close()
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return fmt.Errorf("stderr pipe: %w", err)
+	}
+	defer stderr.Close()
+
+	err = cmd.Start()
+	if err != nil {
+		return fmt.Errorf("start: %w", err)
+	}
+	s.mu.Lock()
+	s.cmd = cmd
+	s.mu.Unlock()
+
+	// read address
+	stdoutScanner := bufio.NewScanner(stdout)
+	stdoutScanner.Scan()
+	if stdoutScanner.Err() != nil {
+		return fmt.Errorf("read addr: %w", stdoutScanner.Err())
+	}
+	addr := stdoutScanner.Text()
+	// send the rest of stdout and stderr to logger to avoid blocking
+	go func() {
+		for stdoutScanner.Scan() {
+			s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
+		}
+	}()
+	stderrScanner := bufio.NewScanner(stderr)
+	go func() {
+		for stderrScanner.Scan() {
+			s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
+		}
+	}()
+	s.mu.Lock()
+	s.addr = strings.TrimSpace(addr)
+	s.mu.Unlock()
+	return cmd.Wait()
+}
+
+var writeMethods = map[string]bool{
+	"PUT":       true,
+	"POST":      true,
+	"COPY":      true,
+	"LOCK":      true,
+	"UNLOCK":    true,
+	"MKCOL":     true,
+	"MOVE":      true,
+	"PROPPATCH": true,
+}

+ 0 - 0
tailfs/shared/pathutil.go → tailfs/tailfsimpl/shared/pathutil.go


+ 0 - 0
tailfs/shared/pathutil_test.go → tailfs/tailfsimpl/shared/pathutil_test.go


+ 0 - 0
tailfs/shared/readonlydir.go → tailfs/tailfsimpl/shared/readonlydir.go


+ 0 - 0
tailfs/shared/stat.go → tailfs/tailfsimpl/shared/stat.go


+ 19 - 18
tailfs/tailfs_test.go → tailfs/tailfsimpl/tailfs_test.go

@@ -1,7 +1,7 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 
-package tailfs
+package tailfsimpl
 
 import (
 	"context"
@@ -20,8 +20,9 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/tailscale/xnet/webdav"
-	"tailscale.com/tailfs/shared"
-	"tailscale.com/tailfs/webdavfs"
+	"tailscale.com/tailfs"
+	"tailscale.com/tailfs/tailfsimpl/shared"
+	"tailscale.com/tailfs/tailfsimpl/webdavfs"
 	"tailscale.com/tstest"
 )
 
@@ -38,10 +39,10 @@ const (
 func init() {
 	// set AllowShareAs() to false so that we don't try to use sub-processes
 	// for access files on disk.
-	disallowShareAs = true
+	tailfs.DisallowShareAs = true
 }
 
-// The tests in this file simulate real-life Tailfs scenarios, but without
+// The tests in this file simulate real-life TailFS scenarios, but without
 // going over the Tailscale network stack.
 func TestDirectoryListing(t *testing.T) {
 	s := newSystem(t)
@@ -51,9 +52,9 @@ func TestDirectoryListing(t *testing.T) {
 	s.checkDirList("root directory should contain the one and only domain once a remote has been set", "/", domain)
 	s.checkDirList("domain should contain its only remote", shared.Join(domain), remote1)
 	s.checkDirList("remote with no shares should be empty", shared.Join(domain, remote1))
-	s.addShare(remote1, share11, PermissionReadWrite)
+	s.addShare(remote1, share11, tailfs.PermissionReadWrite)
 	s.checkDirList("remote with one share should contain that share", shared.Join(domain, remote1), share11)
-	s.addShare(remote1, share12, PermissionReadOnly)
+	s.addShare(remote1, share12, tailfs.PermissionReadOnly)
 	s.checkDirList("remote with two shares should contain both in lexicographical order", shared.Join(domain, remote1), share12, share11)
 	s.checkDirListIncremental("remote with two shares should contain both in lexicographical order even when reading directory incrementally", shared.Join(domain, remote1), share12, share11)
 
@@ -73,12 +74,12 @@ func TestFileManipulation(t *testing.T) {
 	defer s.stop()
 
 	s.addRemote(remote1)
-	s.addShare(remote1, share11, PermissionReadWrite)
+	s.addShare(remote1, share11, tailfs.PermissionReadWrite)
 	s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
 	s.checkFileStatus(remote1, share11, file111)
 	s.checkFileContents(remote1, share11, file111)
 
-	s.addShare(remote1, share12, PermissionReadOnly)
+	s.addShare(remote1, share12, tailfs.PermissionReadOnly)
 	s.writeFile("writing file to read-only remote should fail", remote1, share12, file111, "hello world", false)
 
 	s.writeFile("writing file to non-existent remote should fail", "non-existent", share11, file111, "hello world", false)
@@ -92,7 +93,7 @@ func TestFileOps(t *testing.T) {
 	defer s.stop()
 
 	s.addRemote(remote1)
-	s.addShare(remote1, share11, PermissionReadWrite)
+	s.addShare(remote1, share11, tailfs.PermissionReadWrite)
 	s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
 	fi, err := s.fs.Stat(context.Background(), pathTo(remote1, share11, file111))
 	if err != nil {
@@ -204,7 +205,7 @@ func TestFileRewind(t *testing.T) {
 	defer s.stop()
 
 	s.addRemote(remote1)
-	s.addShare(remote1, share11, PermissionReadWrite)
+	s.addShare(remote1, share11, tailfs.PermissionReadWrite)
 
 	// Create a file slightly longer than our max rewind buffer of 512
 	fileLength := webdavfs.MaxRewindBuffer + 1
@@ -267,7 +268,7 @@ type remote struct {
 	fs          *FileSystemForRemote
 	fileServer  *FileServer
 	shares      map[string]string
-	permissions map[string]Permission
+	permissions map[string]tailfs.Permission
 	mu          sync.RWMutex
 }
 
@@ -343,15 +344,15 @@ func (s *system) addRemote(name string) {
 		fileServer:  fileServer,
 		fs:          NewFileSystemForRemote(log.Printf),
 		shares:      make(map[string]string),
-		permissions: make(map[string]Permission),
+		permissions: make(map[string]tailfs.Permission),
 	}
 	r.fs.SetFileServerAddr(fileServer.Addr())
 	go http.Serve(l, r)
 	s.remotes[name] = r
 
-	remotes := make([]*Remote, 0, len(s.remotes))
+	remotes := make([]*tailfs.Remote, 0, len(s.remotes))
 	for name, r := range s.remotes {
-		remotes = append(remotes, &Remote{
+		remotes = append(remotes, &tailfs.Remote{
 			Name: name,
 			URL:  fmt.Sprintf("http://%s", r.l.Addr()),
 		})
@@ -359,7 +360,7 @@ func (s *system) addRemote(name string) {
 	s.local.fs.SetRemotes(domain, remotes, &http.Transport{})
 }
 
-func (s *system) addShare(remoteName, shareName string, permission Permission) {
+func (s *system) addShare(remoteName, shareName string, permission tailfs.Permission) {
 	r, ok := s.remotes[remoteName]
 	if !ok {
 		s.t.Fatalf("unknown remote %q", remoteName)
@@ -369,9 +370,9 @@ func (s *system) addShare(remoteName, shareName string, permission Permission) {
 	r.shares[shareName] = f
 	r.permissions[shareName] = permission
 
-	shares := make(map[string]*Share, len(r.shares))
+	shares := make(map[string]*tailfs.Share, len(r.shares))
 	for shareName, folder := range r.shares {
-		shares[shareName] = &Share{
+		shares[shareName] = &tailfs.Share{
 			Name: shareName,
 			Path: folder,
 		}

+ 0 - 0
tailfs/webdavfs/readonly_file.go → tailfs/tailfsimpl/webdavfs/readonly_file.go


+ 0 - 0
tailfs/webdavfs/stat_cache.go → tailfs/tailfsimpl/webdavfs/stat_cache.go


+ 1 - 1
tailfs/webdavfs/stat_cache_test.go → tailfs/tailfsimpl/webdavfs/stat_cache_test.go

@@ -10,7 +10,7 @@ import (
 	"testing"
 	"time"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 	"tailscale.com/tstest"
 )
 

+ 1 - 1
tailfs/webdavfs/webdavfs.go → tailfs/tailfsimpl/webdavfs/webdavfs.go

@@ -19,7 +19,7 @@ import (
 	"github.com/tailscale/gowebdav"
 	"github.com/tailscale/xnet/webdav"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 	"tailscale.com/tstime"
 	"tailscale.com/types/logger"
 )

+ 1 - 1
tailfs/webdavfs/writeonly_file.go → tailfs/tailfsimpl/webdavfs/writeonly_file.go

@@ -10,7 +10,7 @@ import (
 	"io/fs"
 	"os"
 
-	"tailscale.com/tailfs/shared"
+	"tailscale.com/tailfs/tailfsimpl/shared"
 )
 
 type writeOnlyFile struct {

+ 16 - 13
tsd/tsd.go

@@ -38,17 +38,18 @@ import (
 
 // System contains all the subsystems of a Tailscale node (tailscaled, etc.)
 type System struct {
-	Dialer         SubSystem[*tsdial.Dialer]
-	DNSManager     SubSystem[*dns.Manager] // can get its *resolver.Resolver from DNSManager.Resolver
-	Engine         SubSystem[wgengine.Engine]
-	NetMon         SubSystem[*netmon.Monitor]
-	MagicSock      SubSystem[*magicsock.Conn]
-	NetstackRouter SubSystem[bool] // using Netstack at all (either entirely or at least for subnets)
-	Router         SubSystem[router.Router]
-	Tun            SubSystem[*tstun.Wrapper]
-	StateStore     SubSystem[ipn.StateStore]
-	Netstack       SubSystem[NetstackImpl] // actually a *netstack.Impl
-	TailfsForLocal SubSystem[*tailfs.FileSystemForLocal]
+	Dialer          SubSystem[*tsdial.Dialer]
+	DNSManager      SubSystem[*dns.Manager] // can get its *resolver.Resolver from DNSManager.Resolver
+	Engine          SubSystem[wgengine.Engine]
+	NetMon          SubSystem[*netmon.Monitor]
+	MagicSock       SubSystem[*magicsock.Conn]
+	NetstackRouter  SubSystem[bool] // using Netstack at all (either entirely or at least for subnets)
+	Router          SubSystem[router.Router]
+	Tun             SubSystem[*tstun.Wrapper]
+	StateStore      SubSystem[ipn.StateStore]
+	Netstack        SubSystem[NetstackImpl] // actually a *netstack.Impl
+	TailFSForLocal  SubSystem[tailfs.FileSystemForLocal]
+	TailFSForRemote SubSystem[tailfs.FileSystemForRemote]
 
 	// InitialConfig is initial server config, if any.
 	// It is nil if the node is not in declarative mode.
@@ -100,8 +101,10 @@ func (s *System) Set(v any) {
 		s.StateStore.Set(v)
 	case NetstackImpl:
 		s.Netstack.Set(v)
-	case *tailfs.FileSystemForLocal:
-		s.TailfsForLocal.Set(v)
+	case tailfs.FileSystemForLocal:
+		s.TailFSForLocal.Set(v)
+	case tailfs.FileSystemForRemote:
+		s.TailFSForRemote.Set(v)
 	default:
 		panic(fmt.Sprintf("unknown type %T", v))
 	}

+ 1 - 1
tsnet/tsnet.go

@@ -530,7 +530,7 @@ func (s *Server) start() (reterr error) {
 	closePool.add(s.dialer)
 	sys.Set(eng)
 
-	// TODO(oxtoacart): do we need to support Tailfs on tsnet, and if so, how?
+	// TODO(oxtoacart): do we need to support TailFS on tsnet, and if so, how?
 	ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
 	if err != nil {
 		return fmt.Errorf("netstack.Create: %w", err)

+ 1 - 1
tstest/integration/tailscaled_deps_test_darwin.go

@@ -38,7 +38,7 @@ import (
 	_ "tailscale.com/ssh/tailssh"
 	_ "tailscale.com/syncs"
 	_ "tailscale.com/tailcfg"
-	_ "tailscale.com/tailfs"
+	_ "tailscale.com/tailfs/tailfsimpl"
 	_ "tailscale.com/tsd"
 	_ "tailscale.com/tsweb/varz"
 	_ "tailscale.com/types/flagtype"

+ 1 - 1
tstest/integration/tailscaled_deps_test_freebsd.go

@@ -38,7 +38,7 @@ import (
 	_ "tailscale.com/ssh/tailssh"
 	_ "tailscale.com/syncs"
 	_ "tailscale.com/tailcfg"
-	_ "tailscale.com/tailfs"
+	_ "tailscale.com/tailfs/tailfsimpl"
 	_ "tailscale.com/tsd"
 	_ "tailscale.com/tsweb/varz"
 	_ "tailscale.com/types/flagtype"

+ 1 - 1
tstest/integration/tailscaled_deps_test_linux.go

@@ -38,7 +38,7 @@ import (
 	_ "tailscale.com/ssh/tailssh"
 	_ "tailscale.com/syncs"
 	_ "tailscale.com/tailcfg"
-	_ "tailscale.com/tailfs"
+	_ "tailscale.com/tailfs/tailfsimpl"
 	_ "tailscale.com/tsd"
 	_ "tailscale.com/tsweb/varz"
 	_ "tailscale.com/types/flagtype"

+ 1 - 1
tstest/integration/tailscaled_deps_test_openbsd.go

@@ -38,7 +38,7 @@ import (
 	_ "tailscale.com/ssh/tailssh"
 	_ "tailscale.com/syncs"
 	_ "tailscale.com/tailcfg"
-	_ "tailscale.com/tailfs"
+	_ "tailscale.com/tailfs/tailfsimpl"
 	_ "tailscale.com/tsd"
 	_ "tailscale.com/tsweb/varz"
 	_ "tailscale.com/types/flagtype"

+ 1 - 1
tstest/integration/tailscaled_deps_test_windows.go

@@ -45,7 +45,7 @@ import (
 	_ "tailscale.com/safesocket"
 	_ "tailscale.com/syncs"
 	_ "tailscale.com/tailcfg"
-	_ "tailscale.com/tailfs"
+	_ "tailscale.com/tailfs/tailfsimpl"
 	_ "tailscale.com/tsd"
 	_ "tailscale.com/tsweb/varz"
 	_ "tailscale.com/types/flagtype"

+ 8 - 8
wgengine/netstack/netstack.go

@@ -133,7 +133,7 @@ type Impl struct {
 	ctxCancel      context.CancelFunc     // called on Close
 	lb             *ipnlocal.LocalBackend // or nil
 	dns            *dns.Manager
-	tailfsForLocal *tailfs.FileSystemForLocal // or nil
+	tailFSForLocal tailfs.FileSystemForLocal // or nil
 
 	peerapiPort4Atomic atomic.Uint32 // uint16 port number for IPv4 peerapi
 	peerapiPort6Atomic atomic.Uint32 // uint16 port number for IPv6 peerapi
@@ -161,7 +161,7 @@ const nicID = 1
 const maxUDPPacketSize = tstun.MaxPacketSize
 
 // Create creates and populates a new Impl.
-func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn, dialer *tsdial.Dialer, dns *dns.Manager, pm *proxymap.Mapper, tailfsForLocal *tailfs.FileSystemForLocal) (*Impl, error) {
+func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn, dialer *tsdial.Dialer, dns *dns.Manager, pm *proxymap.Mapper, tailFSForLocal tailfs.FileSystemForLocal) (*Impl, error) {
 	if mc == nil {
 		return nil, errors.New("nil magicsock.Conn")
 	}
@@ -241,7 +241,7 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
 		dialer:              dialer,
 		connsOpenBySubnetIP: make(map[netip.Addr]int),
 		dns:                 dns,
-		tailfsForLocal:      tailfsForLocal,
+		tailFSForLocal:      tailFSForLocal,
 	}
 	ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
 	ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc())
@@ -443,7 +443,7 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re
 		return filter.DropSilently
 	}
 
-	// If it's not traffic to the service IP (e.g. magicDNS or Tailfs) we don't
+	// If it's not traffic to the service IP (e.g. magicDNS or TailFS) we don't
 	// care; resume processing.
 	if dst := p.Dst.Addr(); dst != serviceIP && dst != serviceIPv6 {
 		return filter.Accept
@@ -922,8 +922,8 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
 	// Local DNS Service (DNS and WebDAV)
 	hittingServiceIP := dialIP == serviceIP || dialIP == serviceIPv6
 	hittingDNS := hittingServiceIP && reqDetails.LocalPort == 53
-	hittingTailfs := hittingServiceIP && ns.tailfsForLocal != nil && reqDetails.LocalPort == 8080
-	if hittingDNS || hittingTailfs {
+	hittingTailFS := hittingServiceIP && ns.tailFSForLocal != nil && reqDetails.LocalPort == 8080
+	if hittingDNS || hittingTailFS {
 		c := getConnOrReset()
 		if c == nil {
 			return
@@ -931,8 +931,8 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
 		addrPort := netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort)
 		if hittingDNS {
 			go ns.dns.HandleTCPConn(c, addrPort)
-		} else if hittingTailfs {
-			err := ns.tailfsForLocal.HandleConn(c, net.TCPAddrFromAddrPort(addrPort))
+		} else if hittingTailFS {
+			err := ns.tailFSForLocal.HandleConn(c, net.TCPAddrFromAddrPort(addrPort))
 			if err != nil {
 				ns.logf("netstack: tailfs.HandleConn: %v", err)
 			}

+ 5 - 5
wgengine/userspace.go

@@ -203,9 +203,9 @@ type Config struct {
 	// SetSubsystem, if non-nil, is called for each new subsystem created, just before a successful return.
 	SetSubsystem func(any)
 
-	// EnableTailfs, if true, will cause the engine to expose a Tailfs listener
-	// at 100.100.100.100:8080
-	EnableTailfs bool
+	// TailFSForLocal, if populated, will cause the engine to expose a TailFS
+	// listener at 100.100.100.100:8080.
+	TailFSForLocal tailfs.FileSystemForLocal
 }
 
 // NewFakeUserspaceEngine returns a new userspace engine for testing.
@@ -451,8 +451,8 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
 		conf.SetSubsystem(conf.Router)
 		conf.SetSubsystem(conf.Dialer)
 		conf.SetSubsystem(e.netMon)
-		if conf.EnableTailfs {
-			conf.SetSubsystem(tailfs.NewFileSystemForLocal(e.logf))
+		if conf.TailFSForLocal != nil {
+			conf.SetSubsystem(conf.TailFSForLocal)
 		}
 	}