From abab0d4197ac73111a62e27de46377d5c8ef0827 Mon Sep 17 00:00:00 2001 From: Percy Wegmann Date: Fri, 9 Feb 2024 11:26:43 -0600 Subject: [PATCH] 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 --- client/tailscale/localclient.go | 20 +- client/tailscale/localclient_test.go | 17 +- cmd/derper/depaware.txt | 13 +- cmd/tailscale/cli/share.go | 6 +- cmd/tailscale/depaware.txt | 16 +- cmd/tailscaled/depaware.txt | 15 +- cmd/tailscaled/tailscaled.go | 28 +- ipn/backend.go | 13 +- ipn/ipnlocal/local.go | 48 +-- ipn/ipnlocal/peerapi.go | 20 +- ipn/ipnlocal/tailfs.go | 99 ++--- ipn/localapi/localapi.go | 16 +- tailcfg/tailcfg.go | 8 +- tailcfg/tailcfg_test.go | 12 + tailfs/local.go | 98 +---- tailfs/remote.go | 391 ++---------------- tailfs/tailfs.go | 18 - tailfs/{ => tailfsimpl}/birthtiming.go | 2 +- tailfs/{ => tailfsimpl}/birthtiming_test.go | 2 +- .../compositefs/compositefs.go | 2 +- .../compositefs/compositefs_test.go | 2 +- tailfs/{ => tailfsimpl}/compositefs/mkdir.go | 2 +- .../{ => tailfsimpl}/compositefs/openfile.go | 2 +- .../{ => tailfsimpl}/compositefs/removeall.go | 2 +- tailfs/{ => tailfsimpl}/compositefs/rename.go | 2 +- tailfs/{ => tailfsimpl}/compositefs/stat.go | 2 +- tailfs/{ => tailfsimpl}/connlistener.go | 2 +- tailfs/{ => tailfsimpl}/connlistener_test.go | 2 +- tailfs/{ => tailfsimpl}/fileserver.go | 6 +- tailfs/tailfsimpl/local_impl.go | 103 +++++ tailfs/tailfsimpl/remote_impl.go | 359 ++++++++++++++++ tailfs/{ => tailfsimpl}/shared/pathutil.go | 0 .../{ => tailfsimpl}/shared/pathutil_test.go | 0 tailfs/{ => tailfsimpl}/shared/readonlydir.go | 0 tailfs/{ => tailfsimpl}/shared/stat.go | 0 tailfs/{ => tailfsimpl}/tailfs_test.go | 37 +- .../webdavfs/readonly_file.go | 0 .../{ => tailfsimpl}/webdavfs/stat_cache.go | 0 .../webdavfs/stat_cache_test.go | 2 +- tailfs/{ => tailfsimpl}/webdavfs/webdavfs.go | 2 +- .../webdavfs/writeonly_file.go | 2 +- tsd/tsd.go | 29 +- tsnet/tsnet.go | 2 +- .../tailscaled_deps_test_darwin.go | 2 +- .../tailscaled_deps_test_freebsd.go | 2 +- .../integration/tailscaled_deps_test_linux.go | 2 +- .../tailscaled_deps_test_openbsd.go | 2 +- .../tailscaled_deps_test_windows.go | 2 +- wgengine/netstack/netstack.go | 16 +- wgengine/userspace.go | 10 +- 50 files changed, 754 insertions(+), 684 deletions(-) delete mode 100644 tailfs/tailfs.go rename tailfs/{ => tailfsimpl}/birthtiming.go (98%) rename tailfs/{ => tailfsimpl}/birthtiming_test.go (99%) rename tailfs/{ => tailfsimpl}/compositefs/compositefs.go (99%) rename tailfs/{ => tailfsimpl}/compositefs/compositefs_test.go (99%) rename tailfs/{ => tailfsimpl}/compositefs/mkdir.go (95%) rename tailfs/{ => tailfsimpl}/compositefs/openfile.go (97%) rename tailfs/{ => tailfsimpl}/compositefs/removeall.go (94%) rename tailfs/{ => tailfsimpl}/compositefs/rename.go (96%) rename tailfs/{ => tailfsimpl}/compositefs/stat.go (97%) rename tailfs/{ => tailfsimpl}/connlistener.go (98%) rename tailfs/{ => tailfsimpl}/connlistener_test.go (98%) rename tailfs/{ => tailfsimpl}/fileserver.go (97%) create mode 100644 tailfs/tailfsimpl/local_impl.go create mode 100644 tailfs/tailfsimpl/remote_impl.go rename tailfs/{ => tailfsimpl}/shared/pathutil.go (100%) rename tailfs/{ => tailfsimpl}/shared/pathutil_test.go (100%) rename tailfs/{ => tailfsimpl}/shared/readonlydir.go (100%) rename tailfs/{ => tailfsimpl}/shared/stat.go (100%) rename tailfs/{ => tailfsimpl}/tailfs_test.go (94%) rename tailfs/{ => tailfsimpl}/webdavfs/readonly_file.go (100%) rename tailfs/{ => tailfsimpl}/webdavfs/stat_cache.go (100%) rename tailfs/{ => tailfsimpl}/webdavfs/stat_cache_test.go (98%) rename tailfs/{ => tailfsimpl}/webdavfs/webdavfs.go (99%) rename tailfs/{ => tailfsimpl}/webdavfs/writeonly_file.go (97%) diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 1b4bec155..9efee2780 100644 --- a/client/tailscale/localclient.go +++ b/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 diff --git a/client/tailscale/localclient_test.go b/client/tailscale/localclient_test.go index ebcd1bab6..ca2198e2b 100644 --- a/client/tailscale/localclient_test.go +++ b/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) +} diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 4496cc585..b130f917a 100644 --- a/cmd/derper/depaware.txt +++ b/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+ diff --git a/cmd/tailscale/cli/share.go b/cmd/tailscale/cli/share.go index 183fa33d0..d93116423 100644 --- a/cmd/tailscale/cli/share.go +++ b/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 } diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index a9cad3c25..f9abb56fb 100644 --- a/cmd/tailscale/depaware.txt +++ b/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+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index e45d20b75..45646fa9f 100644 --- a/cmd/tailscaled/depaware.txt +++ b/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 diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 5b1a501c8..1f9d8ba8f 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/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 pairs") } - s, err := tailfs.NewFileServer() + s, err := tailfsimpl.NewFileServer() if err != nil { return fmt.Errorf("unable to start tailfs FileServer: %v", err) } diff --git a/ipn/backend.go b/ipn/backend.go index f527a458a..787f026ff 100644 --- a/ipn/backend.go +++ b/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 } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c275190ae..5c1a025ab 100644 --- a/ipn/ipnlocal/local.go +++ b/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) diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 1adbae1f2..5d0a3d386 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/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) } diff --git a/ipn/ipnlocal/tailfs.go b/ipn/ipnlocal/tailfs.go index 4d69673f4..da5ec080e 100644 --- a/ipn/ipnlocal/tailfs.go +++ b/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}) } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 191558a1b..a1b7da46f 100644 --- a/ipn/localapi/localapi.go +++ b/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 diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b9167dc32..c036c8a2e 100644 --- a/tailcfg/tailcfg.go +++ b/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. diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index bf8e48bdd..294af47be 100644 --- a/tailcfg/tailcfg_test.go +++ b/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) +} diff --git a/tailfs/local.go b/tailfs/local.go index 0c8ef16fd..67f0c3fe5 100644 --- a/tailfs/local.go +++ b/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 } diff --git a/tailfs/remote.go b/tailfs/remote.go index 17f8dfa75..63f5997c6 100644 --- a/tailfs/remote.go +++ b/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 } diff --git a/tailfs/tailfs.go b/tailfs/tailfs.go deleted file mode 100644 index 04c51ecdc..000000000 --- a/tailfs/tailfs.go +++ /dev/null @@ -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 -) diff --git a/tailfs/birthtiming.go b/tailfs/tailfsimpl/birthtiming.go similarity index 98% rename from tailfs/birthtiming.go rename to tailfs/tailfsimpl/birthtiming.go index fc4f969ca..8e7f8c965 100644 --- a/tailfs/birthtiming.go +++ b/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" diff --git a/tailfs/birthtiming_test.go b/tailfs/tailfsimpl/birthtiming_test.go similarity index 99% rename from tailfs/birthtiming_test.go rename to tailfs/tailfsimpl/birthtiming_test.go index df25c013e..7054c783b 100644 --- a/tailfs/birthtiming_test.go +++ b/tailfs/tailfsimpl/birthtiming_test.go @@ -5,7 +5,7 @@ //go:build windows || darwin -package tailfs +package tailfsimpl import ( "context" diff --git a/tailfs/compositefs/compositefs.go b/tailfs/tailfsimpl/compositefs/compositefs.go similarity index 99% rename from tailfs/compositefs/compositefs.go rename to tailfs/tailfsimpl/compositefs/compositefs.go index ec00dce30..f4be56bd3 100644 --- a/tailfs/compositefs/compositefs.go +++ b/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" ) diff --git a/tailfs/compositefs/compositefs_test.go b/tailfs/tailfsimpl/compositefs/compositefs_test.go similarity index 99% rename from tailfs/compositefs/compositefs_test.go rename to tailfs/tailfsimpl/compositefs/compositefs_test.go index 3409d764a..cabf39e9c 100644 --- a/tailfs/compositefs/compositefs_test.go +++ b/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" ) diff --git a/tailfs/compositefs/mkdir.go b/tailfs/tailfsimpl/compositefs/mkdir.go similarity index 95% rename from tailfs/compositefs/mkdir.go rename to tailfs/tailfsimpl/compositefs/mkdir.go index 35c7b18b0..4afd61753 100644 --- a/tailfs/compositefs/mkdir.go +++ b/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 diff --git a/tailfs/compositefs/openfile.go b/tailfs/tailfsimpl/compositefs/openfile.go similarity index 97% rename from tailfs/compositefs/openfile.go rename to tailfs/tailfsimpl/compositefs/openfile.go index afa388688..dd04a6c18 100644 --- a/tailfs/compositefs/openfile.go +++ b/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. diff --git a/tailfs/compositefs/removeall.go b/tailfs/tailfsimpl/compositefs/removeall.go similarity index 94% rename from tailfs/compositefs/removeall.go rename to tailfs/tailfsimpl/compositefs/removeall.go index fd07ef79b..7f5e80f85 100644 --- a/tailfs/compositefs/removeall.go +++ b/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, diff --git a/tailfs/compositefs/rename.go b/tailfs/tailfsimpl/compositefs/rename.go similarity index 96% rename from tailfs/compositefs/rename.go rename to tailfs/tailfsimpl/compositefs/rename.go index 2fcc3bd3d..1c1a62e2f 100644 --- a/tailfs/compositefs/rename.go +++ b/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 diff --git a/tailfs/compositefs/stat.go b/tailfs/tailfsimpl/compositefs/stat.go similarity index 97% rename from tailfs/compositefs/stat.go rename to tailfs/tailfsimpl/compositefs/stat.go index c117e8809..78ca70abf 100644 --- a/tailfs/compositefs/stat.go +++ b/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. diff --git a/tailfs/connlistener.go b/tailfs/tailfsimpl/connlistener.go similarity index 98% rename from tailfs/connlistener.go rename to tailfs/tailfsimpl/connlistener.go index a6405109c..6ddb1adab 100644 --- a/tailfs/connlistener.go +++ b/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" diff --git a/tailfs/connlistener_test.go b/tailfs/tailfsimpl/connlistener_test.go similarity index 98% rename from tailfs/connlistener_test.go rename to tailfs/tailfsimpl/connlistener_test.go index 274aef8a2..6267d837e 100644 --- a/tailfs/connlistener_test.go +++ b/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" diff --git a/tailfs/fileserver.go b/tailfs/tailfsimpl/fileserver.go similarity index 97% rename from tailfs/fileserver.go rename to tailfs/tailfsimpl/fileserver.go index d392b4a51..73ea08b6c 100644 --- a/tailfs/fileserver.go +++ b/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 diff --git a/tailfs/tailfsimpl/local_impl.go b/tailfs/tailfsimpl/local_impl.go new file mode 100644 index 000000000..921d9a2b5 --- /dev/null +++ b/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() +} diff --git a/tailfs/tailfsimpl/remote_impl.go b/tailfs/tailfsimpl/remote_impl.go new file mode 100644 index 000000000..da9cde0f3 --- /dev/null +++ b/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, +} diff --git a/tailfs/shared/pathutil.go b/tailfs/tailfsimpl/shared/pathutil.go similarity index 100% rename from tailfs/shared/pathutil.go rename to tailfs/tailfsimpl/shared/pathutil.go diff --git a/tailfs/shared/pathutil_test.go b/tailfs/tailfsimpl/shared/pathutil_test.go similarity index 100% rename from tailfs/shared/pathutil_test.go rename to tailfs/tailfsimpl/shared/pathutil_test.go diff --git a/tailfs/shared/readonlydir.go b/tailfs/tailfsimpl/shared/readonlydir.go similarity index 100% rename from tailfs/shared/readonlydir.go rename to tailfs/tailfsimpl/shared/readonlydir.go diff --git a/tailfs/shared/stat.go b/tailfs/tailfsimpl/shared/stat.go similarity index 100% rename from tailfs/shared/stat.go rename to tailfs/tailfsimpl/shared/stat.go diff --git a/tailfs/tailfs_test.go b/tailfs/tailfsimpl/tailfs_test.go similarity index 94% rename from tailfs/tailfs_test.go rename to tailfs/tailfsimpl/tailfs_test.go index e10384cc4..b03eae17c 100644 --- a/tailfs/tailfs_test.go +++ b/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, } diff --git a/tailfs/webdavfs/readonly_file.go b/tailfs/tailfsimpl/webdavfs/readonly_file.go similarity index 100% rename from tailfs/webdavfs/readonly_file.go rename to tailfs/tailfsimpl/webdavfs/readonly_file.go diff --git a/tailfs/webdavfs/stat_cache.go b/tailfs/tailfsimpl/webdavfs/stat_cache.go similarity index 100% rename from tailfs/webdavfs/stat_cache.go rename to tailfs/tailfsimpl/webdavfs/stat_cache.go diff --git a/tailfs/webdavfs/stat_cache_test.go b/tailfs/tailfsimpl/webdavfs/stat_cache_test.go similarity index 98% rename from tailfs/webdavfs/stat_cache_test.go rename to tailfs/tailfsimpl/webdavfs/stat_cache_test.go index 3646382a5..b22582cc5 100644 --- a/tailfs/webdavfs/stat_cache_test.go +++ b/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" ) diff --git a/tailfs/webdavfs/webdavfs.go b/tailfs/tailfsimpl/webdavfs/webdavfs.go similarity index 99% rename from tailfs/webdavfs/webdavfs.go rename to tailfs/tailfsimpl/webdavfs/webdavfs.go index 11f71a19b..defd9c52e 100644 --- a/tailfs/webdavfs/webdavfs.go +++ b/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" ) diff --git a/tailfs/webdavfs/writeonly_file.go b/tailfs/tailfsimpl/webdavfs/writeonly_file.go similarity index 97% rename from tailfs/webdavfs/writeonly_file.go rename to tailfs/tailfsimpl/webdavfs/writeonly_file.go index ce667e497..544ce9262 100644 --- a/tailfs/webdavfs/writeonly_file.go +++ b/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 { diff --git a/tsd/tsd.go b/tsd/tsd.go index f70b6d059..21ebb6a4a 100644 --- a/tsd/tsd.go +++ b/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)) } diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 705770891..6edbc5ec9 100644 --- a/tsnet/tsnet.go +++ b/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) diff --git a/tstest/integration/tailscaled_deps_test_darwin.go b/tstest/integration/tailscaled_deps_test_darwin.go index 1479cfe53..ad47de941 100644 --- a/tstest/integration/tailscaled_deps_test_darwin.go +++ b/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" diff --git a/tstest/integration/tailscaled_deps_test_freebsd.go b/tstest/integration/tailscaled_deps_test_freebsd.go index 1479cfe53..ad47de941 100644 --- a/tstest/integration/tailscaled_deps_test_freebsd.go +++ b/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" diff --git a/tstest/integration/tailscaled_deps_test_linux.go b/tstest/integration/tailscaled_deps_test_linux.go index 1479cfe53..ad47de941 100644 --- a/tstest/integration/tailscaled_deps_test_linux.go +++ b/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" diff --git a/tstest/integration/tailscaled_deps_test_openbsd.go b/tstest/integration/tailscaled_deps_test_openbsd.go index 1479cfe53..ad47de941 100644 --- a/tstest/integration/tailscaled_deps_test_openbsd.go +++ b/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" diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go index 1f19b5275..0f84a46b2 100644 --- a/tstest/integration/tailscaled_deps_test_windows.go +++ b/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" diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 338b2ad84..8e433a202 100644 --- a/wgengine/netstack/netstack.go +++ b/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) } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index f0fcafc5b..525af32a9 100644 --- a/wgengine/userspace.go +++ b/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) } }