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 <percy@tailscale.com>
This commit is contained in:
Percy Wegmann 2024-02-09 11:26:43 -06:00 committed by Percy Wegmann
parent 79b547804b
commit abab0d4197
50 changed files with 753 additions and 683 deletions

View File

@ -1418,25 +1418,25 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
return &cv, nil 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 // 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. // TailFS know to use the file server running in the GUI app.
func (lc *LocalClient) TailfsSetFileServerAddr(ctx context.Context, addr string) error { 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)) _, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
return err 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 // serve to remote nodes. If a share with the same name already exists, the
// existing share is replaced/updated. // 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)) _, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
return err return err
} }
// TailfsShareRemove removes the share with the given name from the list of // TailFSShareRemove removes the share with the given name from the list of
// shares that Tailfs will serve to remote nodes. // shares that TailFS will serve to remote nodes.
func (lc *LocalClient) TailfsShareRemove(ctx context.Context, name string) error { func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error {
_, err := lc.send( _, err := lc.send(
ctx, ctx,
"DELETE", "DELETE",
@ -1448,9 +1448,9 @@ func (lc *LocalClient) TailfsShareRemove(ctx context.Context, name string) error
return err 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. // 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") result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -5,7 +5,11 @@
package tailscale package tailscale
import "testing" import (
"testing"
"tailscale.com/tstest/deptest"
)
func TestGetServeConfigFromJSON(t *testing.T) { func TestGetServeConfigFromJSON(t *testing.T) {
sc, err := getServeConfigFromJSON([]byte("null")) sc, err := getServeConfigFromJSON([]byte("null"))
@ -25,3 +29,14 @@ func TestGetServeConfigFromJSON(t *testing.T) {
t.Errorf("want non-nil TCP for object") 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)
}

View File

@ -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 💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil 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/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/google/nftables from tailscale.com/util/linuxfw 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+ L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/tsweb github.com/google/uuid from tailscale.com/tsweb
github.com/hdevalence/ed25519consensus from tailscale.com/tka 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/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink L github.com/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/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/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+ 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 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/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns 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 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/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper+ tailscale.com/net/wsconn from tailscale.com/cmd/derper+
tailscale.com/paths from tailscale.com/client/tailscale 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/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+ tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailfs 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+ tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/derp+ 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 golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+ D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from github.com/mdlayher/socket+ 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+ golang.org/x/sys/cpu from github.com/josharian/native+
LD golang.org/x/sys/unix from github.com/google/nftables+ LD golang.org/x/sys/unix from github.com/google/nftables+
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+ W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
@ -205,7 +196,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
cmp from slices+ cmp from slices+
compress/flate from compress/gzip+ compress/flate from compress/gzip+
compress/gzip from google.golang.org/protobuf/internal/impl+ compress/gzip from google.golang.org/protobuf/internal/impl+
container/heap from github.com/jellydator/ttlcache/v3+
container/list from crypto/tls+ container/list from crypto/tls+
context from crypto/tls+ context from crypto/tls+
crypto from crypto/ecdh+ 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/hex from crypto/x509+
encoding/json from expvar+ encoding/json from expvar+
encoding/pem from crypto/tls+ encoding/pem from crypto/tls+
encoding/xml from github.com/tailscale/gowebdav+
errors from bufio+ errors from bufio+
expvar from github.com/prometheus/client_golang/prometheus+ expvar from github.com/prometheus/client_golang/prometheus+
flag from tailscale.com/cmd/derper+ flag from tailscale.com/cmd/derper+

View File

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

View File

@ -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 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 from github.com/dblohm7/wingoes/pe+
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode 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 github.com/fxamacker/cbor/v2 from tailscale.com/tka
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus
github.com/golang/groupcache/lru from tailscale.com/net/dnscache github.com/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/csrf from tailscale.com/client/web
github.com/gorilla/securecookie from github.com/gorilla/csrf github.com/gorilla/securecookie from github.com/gorilla/csrf
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+ 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/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink L github.com/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/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap 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/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 L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web 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/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink L 💣 github.com/vishvananda/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/safesocket from tailscale.com/client/tailscale+
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+ tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
tailscale.com/tailcfg from tailscale.com/client/tailscale+ tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailfs from tailscale.com/client/tailscale+ tailscale.com/tailfs from tailscale.com/cmd/tailscale/cli+
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+ tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/control/controlhttp+ 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/clientcredentials from tailscale.com/cmd/tailscale/cli
golang.org/x/oauth2/internal from golang.org/x/oauth2+ golang.org/x/oauth2/internal from golang.org/x/oauth2+
golang.org/x/sync/errgroup from github.com/mdlayher/socket+ 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+ golang.org/x/sys/cpu from github.com/josharian/native+
LD golang.org/x/sys/unix from github.com/google/nftables+ LD golang.org/x/sys/unix from github.com/google/nftables+
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+ W golang.org/x/sys/windows 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/flate from compress/gzip+
compress/gzip from net/http+ compress/gzip from net/http+
compress/zlib from debug/pe+ compress/zlib from debug/pe+
container/heap from github.com/jellydator/ttlcache/v3+
container/list from crypto/tls+ container/list from crypto/tls+
context from crypto/tls+ context from crypto/tls+
crypto from crypto/ecdh+ 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/big from crypto/dsa+
math/bits from compress/flate+ math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+ 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/multipart from net/http
mime/quotedprintable from mime/multipart mime/quotedprintable from mime/multipart
net from crypto/tls+ net from crypto/tls+
@ -306,7 +296,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
reflect from archive/tar+ reflect from archive/tar+
regexp from github.com/coreos/go-iptables/iptables+ regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight+ runtime/debug from nhooyr.io/websocket/internal/xsync+
runtime/trace from testing runtime/trace from testing
slices from tailscale.com/client/web+ slices from tailscale.com/client/web+
sort from archive/tar+ sort from archive/tar+

View File

@ -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/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+ W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture 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 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 from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet 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/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces 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 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/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/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ 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/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap 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/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 github.com/tailscale/hujson from tailscale.com/ipn/conffile
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+ L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web 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/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/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun 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/tailscale/xnet/webdav/internal/xml from github.com/tailscale/xnet/webdav
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh 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/tailcfg from tailscale.com/client/tailscale+
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+ tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
tailscale.com/tailfs from tailscale.com/client/tailscale+ tailscale.com/tailfs from tailscale.com/client/tailscale+
tailscale.com/tailfs/compositefs from tailscale.com/tailfs tailscale.com/tailfs/tailfsimpl from tailscale.com/cmd/tailscaled
tailscale.com/tailfs/shared from tailscale.com/tailfs/compositefs+ tailscale.com/tailfs/tailfsimpl/compositefs from tailscale.com/tailfs/tailfsimpl
tailscale.com/tailfs/webdavfs from tailscale.com/tailfs 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 💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock

View File

@ -52,7 +52,7 @@
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/syncs" "tailscale.com/syncs"
"tailscale.com/tailfs" "tailscale.com/tailfs/tailfsimpl"
"tailscale.com/tsd" "tailscale.com/tsd"
"tailscale.com/tsweb/varz" "tailscale.com/tsweb/varz"
"tailscale.com/types/flagtype" "tailscale.com/types/flagtype"
@ -141,7 +141,7 @@ func defaultPort() uint16 {
"uninstall-system-daemon": uninstallSystemDaemon, "uninstall-system-daemon": uninstallSystemDaemon,
"debug": debugModeFunc, "debug": debugModeFunc,
"be-child": beChild, "be-child": beChild,
"serve-tailfs": serveTailfs, "serve-tailfs": serveTailFS,
} }
var beCLI func() // non-nil if CLI is linked in var beCLI func() // non-nil if CLI is linked in
@ -403,6 +403,8 @@ func run() (err error) {
debugMux = newDebugMux() debugMux = newDebugMux()
} }
sys.Set(tailfsimpl.NewFileSystemForRemote(logf))
return startIPNServer(context.Background(), logf, pol.PublicID, sys) return startIPNServer(context.Background(), logf, pol.PublicID, sys)
} }
@ -625,12 +627,12 @@ func handleSubnetsInNetstack() bool {
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) { func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
conf := wgengine.Config{ conf := wgengine.Config{
ListenPort: args.port, ListenPort: args.port,
NetMon: sys.NetMon.Get(), NetMon: sys.NetMon.Get(),
Dialer: sys.Dialer.Get(), Dialer: sys.Dialer.Get(),
SetSubsystem: sys.Set, SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(), ControlKnobs: sys.ControlKnobs(),
EnableTailfs: true, TailFSForLocal: tailfsimpl.NewFileSystemForLocal(logf),
} }
onlyNetstack = name == "userspace-networking" 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) { func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
tfs, _ := sys.TailfsForLocal.GetOK() tfs, _ := sys.TailFSForLocal.GetOK()
ret, err := netstack.Create(logf, ret, err := netstack.Create(logf,
sys.Tun.Get(), sys.Tun.Get(),
sys.Engine.Get(), sys.Engine.Get(),
@ -809,21 +811,21 @@ func beChild(args []string) error {
return f(args[1:]) 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 // protocol. On UNIX and MacOS tailscaled environment, tailfs spawns child
// tailscaled processes in serve-tailfs mode in order to access the fliesystem // tailscaled processes in serve-tailfs mode in order to access the fliesystem
// as specific (usually unprivileged) users. // 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. // parent process knows where to connect to.
func serveTailfs(args []string) error { func serveTailFS(args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("missing shares") return errors.New("missing shares")
} }
if len(args)%2 != 0 { if len(args)%2 != 0 {
return errors.New("need <sharename> <path> pairs") return errors.New("need <sharename> <path> pairs")
} }
s, err := tailfs.NewFileServer() s, err := tailfsimpl.NewFileServer()
if err != nil { if err != nil {
return fmt.Errorf("unable to start tailfs FileServer: %v", err) return fmt.Errorf("unable to start tailfs FileServer: %v", err)
} }

View File

@ -66,7 +66,7 @@ type EngineStatus struct {
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
NotifyInitialTailfsShares // if set, the first Notify message (sent immediately) will contain the current Tailfs Shares 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 // Notify is a communication from a backend (e.g. tailscaled) to a frontend
@ -122,11 +122,12 @@ type Notify struct {
// is available. // is available.
ClientVersion *tailcfg.ClientVersion `json:",omitempty"` ClientVersion *tailcfg.ClientVersion `json:",omitempty"`
// Full set of current TailfsShares that we're publishing as name->path. // TailFSShares tracks the full set of current TailFSShares that we're
// Some client applications, like the MacOS and Windows clients, will // publishing as name->path. Some client applications, like the MacOS and
// listen for updates to this and handle serving these shares under the // Windows clients, will listen for updates to this and handle serving
// identity of the unprivileged user that is running the application. // these shares under the identity of the unprivileged user that is running
TailfsShares map[string]string `json:",omitempty"` // the application.
TailFSShares map[string]string `json:",omitempty"`
// type is mirrored in xcode/Shared/IPN.swift // type is mirrored in xcode/Shared/IPN.swift
} }

View File

@ -67,7 +67,6 @@
"tailscale.com/syncs" "tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/taildrop" "tailscale.com/taildrop"
"tailscale.com/tailfs"
"tailscale.com/tka" "tailscale.com/tka"
"tailscale.com/tsd" "tailscale.com/tsd"
"tailscale.com/tstime" "tailscale.com/tstime"
@ -288,8 +287,7 @@ type LocalBackend struct {
serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
tailfsListeners map[netip.AddrPort]*localListener // listeners for local tailfs traffic tailFSListeners map[netip.AddrPort]*localListener // listeners for local tailfs traffic
tailfsForRemote *tailfs.FileSystemForRemote
// statusLock must be held before calling statusChanged.Wait() or // statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast(). // statusChanged.Broadcast().
@ -432,13 +430,15 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
} }
} }
// initialize Tailfs shares from saved state // initialize TailFS shares from saved state
b.mu.Lock() fs, ok := b.sys.TailFSForRemote.GetOK()
b.tailfsForRemote = tailfs.NewFileSystemForRemote(logf) if !ok {
shares, err := b.tailfsGetSharesLocked() b.mu.Lock()
b.mu.Unlock() shares, err := b.tailFSGetSharesLocked()
if err == nil && len(shares) > 0 { b.mu.Unlock()
b.tailfsForRemote.SetShares(shares) if err == nil && len(shares) > 0 {
fs.SetShares(shares)
}
} }
return b, nil return b, nil
@ -2268,7 +2268,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
b.mu.Lock() b.mu.Lock()
b.activeWatchSessions.Add(sessionID) 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 { if mask&initialBits != 0 {
ini = &ipn.Notify{Version: version.Long()} ini = &ipn.Notify{Version: version.Long()}
if mask&ipn.NotifyInitialState != 0 { if mask&ipn.NotifyInitialState != 0 {
@ -2284,14 +2284,14 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
if mask&ipn.NotifyInitialNetMap != 0 { if mask&ipn.NotifyInitialNetMap != 0 {
ini.NetMap = b.netMap ini.NetMap = b.netMap
} }
if mask&ipn.NotifyInitialTailfsShares != 0 && b.tailfsSharingEnabledLocked() { if mask&ipn.NotifyInitialTailFSShares != 0 && b.tailFSSharingEnabledLocked() {
shares, err := b.tailfsGetSharesLocked() shares, err := b.tailFSGetSharesLocked()
if err != nil { if err != nil {
b.logf("unable to notify initial tailfs shares: %v", err) b.logf("unable to notify initial tailfs shares: %v", err)
} else { } else {
ini.TailfsShares = make(map[string]string, len(shares)) ini.TailFSShares = make(map[string]string, len(shares))
for _, share := range 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() { if dst.Port() == webClientPort && b.ShouldRunWebClient() {
return b.handleWebClientConn, opts return b.handleWebClientConn, opts
} }
if dst.Port() == TailfsLocalPort { if dst.Port() == TailFSLocalPort {
fs, ok := b.sys.TailfsForLocal.GetOK() fs, ok := b.sys.TailFSForLocal.GetOK()
if ok { if ok {
return func(conn net.Conn) error { return func(conn net.Conn) error {
return fs.HandleConn(conn, conn.RemoteAddr()) return fs.HandleConn(conn, conn.RemoteAddr())
@ -4642,9 +4642,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
} }
} }
if b.tailfsSharingEnabledLocked() { if b.tailFSSharingEnabledLocked() {
b.updateTailfsPeersLocked(nm) b.updateTailFSPeersLocked(nm)
b.tailfsNotifyCurrentSharesLocked() 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 // b.Dialer().PeerAPITransport() for each round trip and imposes a short
// dial timeout to avoid hanging on connecting to offline/unreachable hosts. // dial timeout to avoid hanging on connecting to offline/unreachable hosts.
type tailfsTransport struct { type tailFSTransport struct {
b *LocalBackend 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 // dialTimeout is fairly aggressive to avoid hangs on contacting offline or
// unreachable hosts. // unreachable hosts.
dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this
@ -4767,7 +4767,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
} }
if !b.sys.IsNetstack() { if !b.sys.IsNetstack() {
b.updateTailfsListenersLocked() b.updateTailFSListenersLocked()
} }
b.reloadServeConfigLocked(prefs) b.reloadServeConfigLocked(prefs)

View File

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

View File

@ -24,9 +24,9 @@
) )
const ( 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. // connections on quad 100.
TailfsLocalPort = 8080 TailFSLocalPort = 8080
tailfsSharesStateKey = ipn.StateKey("_tailfs-shares") tailfsSharesStateKey = ipn.StateKey("_tailfs-shares")
) )
@ -36,27 +36,25 @@
errInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces") 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 // enabled. This is currently based on checking for the tailfs:share node
// attribute. // attribute.
func (b *LocalBackend) TailfsSharingEnabled() bool { func (b *LocalBackend) TailFSSharingEnabled() bool {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
return b.tailfsSharingEnabledLocked() return b.tailFSSharingEnabledLocked()
} }
func (b *LocalBackend) tailfsSharingEnabledLocked() bool { func (b *LocalBackend) tailFSSharingEnabledLocked() bool {
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailfsSharingEnabled) 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 // to the tailfs.FileServer that's exposing local files as an unprivileged
// user. // user.
func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error { func (b *LocalBackend) TailFSSetFileServerAddr(addr string) error {
b.mu.Lock() fs, ok := b.sys.TailFSForRemote.GetOK()
fs := b.tailfsForRemote if !ok {
b.mu.Unlock()
if fs == nil {
return errors.New("tailfs not enabled") return errors.New("tailfs not enabled")
} }
@ -64,11 +62,11 @@ func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error {
return nil 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. // replaces the existing share if one with the same name already exists.
// To avoid potential incompatibilities across file systems, share names are // To avoid potential incompatibilities across file systems, share names are
// limited to alphanumeric characters and the underscore _. // 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 var err error
share.Name, err = normalizeShareName(share.Name) share.Name, err = normalizeShareName(share.Name)
if err != nil { if err != nil {
@ -104,11 +102,12 @@ func normalizeShareName(name string) (string, error) {
} }
func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[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") return nil, errors.New("tailfs not enabled")
} }
shares, err := b.tailfsGetSharesLocked() shares, err := b.tailFSGetSharesLocked()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -121,17 +120,21 @@ func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]str
if err != nil { if err != nil {
return nil, fmt.Errorf("write state: %w", err) return nil, fmt.Errorf("write state: %w", err)
} }
b.tailfsForRemote.SetShares(shares) fs.SetShares(shares)
return shareNameMap(shares), nil 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. // 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 // Force all share names to lowercase to avoid potential incompatibilities
// with clients that don't support case-sensitive filenames. // 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() b.mu.Lock()
shares, err := b.tailfsRemoveShareLocked(name) 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) { 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") return nil, errors.New("tailfs not enabled")
} }
shares, err := b.tailfsGetSharesLocked() shares, err := b.tailFSGetSharesLocked()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -166,7 +170,7 @@ func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string,
if err != nil { if err != nil {
return nil, fmt.Errorf("write state: %w", err) return nil, fmt.Errorf("write state: %w", err)
} }
b.tailfsForRemote.SetShares(shares) fs.SetShares(shares)
return shareNameMap(shares), nil 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) // tailfsNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
// about the latest set of shares, supplied as a map of name -> directory. // about the latest set of shares, supplied as a map of name -> directory.
func (b *LocalBackend) tailfsNotifyShares(shares map[string]string) { 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 // tailFSNotifyCurrentSharesLocked sends an ipn.Notify with the current set of
// tailfs shares. // TailFS shares.
func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() { func (b *LocalBackend) tailFSNotifyCurrentSharesLocked() {
shares, err := b.tailfsGetSharesLocked() shares, err := b.tailFSGetSharesLocked()
if err != nil { if err != nil {
b.logf("error notifying current tailfs shares: %v", err) b.logf("error notifying current tailfs shares: %v", err)
return return
@ -197,15 +201,16 @@ func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() {
go b.tailfsNotifyShares(shareNameMap(shares)) go b.tailfsNotifyShares(shareNameMap(shares))
} }
// TailfsGetShares() returns the current set of shares from the state store. // TailFSGetShares returns the current set of shares from the state store,
func (b *LocalBackend) TailfsGetShares() (map[string]*tailfs.Share, error) { // stored under ipn.StateKey("_tailfs-shares").
func (b *LocalBackend) TailFSGetShares() (map[string]*tailfs.Share, error) {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() 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) data, err := b.store.ReadState(tailfsSharesStateKey)
if err != nil { if err != nil {
if errors.Is(err, ipn.ErrStateNotExist) { if errors.Is(err, ipn.ErrStateNotExist) {
@ -223,27 +228,27 @@ func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error)
return shares, nil 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 // This is needed to properly route local traffic when using kernel networking
// mode. // mode.
func (b *LocalBackend) updateTailfsListenersLocked() { func (b *LocalBackend) updateTailFSListenersLocked() {
if b.netMap == nil { if b.netMap == nil {
return return
} }
addrs := b.netMap.GetAddresses() addrs := b.netMap.GetAddresses()
oldListeners := b.tailfsListeners oldListeners := b.tailFSListeners
newListeners := make(map[netip.AddrPort]*localListener, addrs.Len()) newListeners := make(map[netip.AddrPort]*localListener, addrs.Len())
for i := range addrs.LenIter() { for i := range addrs.LenIter() {
if fs, ok := b.sys.TailfsForLocal.GetOK(); ok { if fs, ok := b.sys.TailFSForLocal.GetOK(); ok {
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), TailfsLocalPort) addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), TailFSLocalPort)
if sl, ok := b.tailfsListeners[addrPort]; ok { if sl, ok := b.tailFSListeners[addrPort]; ok {
newListeners[addrPort] = sl newListeners[addrPort] = sl
delete(oldListeners, addrPort) delete(oldListeners, addrPort)
continue // already listening continue // already listening
} }
sl := b.newTailfsListener(context.Background(), fs, addrPort, b.logf) sl := b.newTailFSListener(context.Background(), fs, addrPort, b.logf)
newListeners[addrPort] = sl newListeners[addrPort] = sl
go sl.Run() 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. // 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) ctx, cancel := context.WithCancel(ctx)
return &localListener{ return &localListener{
b: b, 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. // remotes.
func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) { func (b *LocalBackend) updateTailFSPeersLocked(nm *netmap.NetworkMap) {
fs, ok := b.sys.TailfsForLocal.GetOK() fs, ok := b.sys.TailFSForLocal.GetOK()
if !ok { if !ok {
return return
} }
@ -284,7 +289,7 @@ func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
tailfsRemotes := make([]*tailfs.Remote, 0, len(nm.Peers)) tailfsRemotes := make([]*tailfs.Remote, 0, len(nm.Peers))
for _, p := range nm.Peers { for _, p := range nm.Peers {
peerID := p.ID() 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{ tailfsRemotes = append(tailfsRemotes, &tailfs.Remote{
Name: p.DisplayName(false), Name: p.DisplayName(false),
URL: url, 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})
} }

View File

@ -110,7 +110,7 @@
"serve-config": (*Handler).serveServeConfig, "serve-config": (*Handler).serveServeConfig,
"set-dns": (*Handler).serveSetDNS, "set-dns": (*Handler).serveSetDNS,
"set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-expiry-sooner": (*Handler).serveSetExpirySooner,
"tailfs/fileserver-address": (*Handler).serveTailfsFileServerAddr, "tailfs/fileserver-address": (*Handler).serveTailFSFileServerAddr,
"tailfs/shares": (*Handler).serveShares, "tailfs/shares": (*Handler).serveShares,
"start": (*Handler).serveStart, "start": (*Handler).serveStart,
"status": (*Handler).serveStatus, "status": (*Handler).serveStatus,
@ -2531,8 +2531,8 @@ func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(ups) json.NewEncoder(w).Encode(ups)
} }
// serveTailfsFileServerAddr handles updates of the tailfs file server address. // serveTailFSFileServerAddr handles updates of the tailfs file server address.
func (h *Handler) serveTailfsFileServerAddr(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveTailFSFileServerAddr(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" { if r.Method != "PUT" {
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed) http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
return return
@ -2544,13 +2544,13 @@ func (h *Handler) serveTailfsFileServerAddr(w http.ResponseWriter, r *http.Reque
return return
} }
h.b.TailfsSetFileServerAddr(string(b)) h.b.TailFSSetFileServerAddr(string(b))
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
} }
// serveShares handles the management of tailfs shares. // serveShares handles the management of tailfs shares.
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, `tailfs sharing not enabled, please add the attribute "tailfs:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusInternalServerError)
return return
} }
@ -2581,7 +2581,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
} }
share.As = username share.As = username
} }
err = h.b.TailfsAddShare(&share) err = h.b.TailFSAddShare(&share)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -2594,7 +2594,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
err = h.b.TailfsRemoveShare(share.Name) err = h.b.TailFSRemoveShare(share.Name)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
http.Error(w, "share not found", http.StatusNotFound) 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) w.WriteHeader(http.StatusNoContent)
case "GET": case "GET":
shares, err := h.b.TailfsGetShares() shares, err := h.b.TailFSGetShares()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -1345,8 +1345,8 @@ type CapGrant struct {
// PeerCapabilityWebUI grants the ability for a peer to edit features from the // PeerCapabilityWebUI grants the ability for a peer to edit features from the
// device Web UI. // device Web UI.
PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui" PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui"
// PeerCapabilityTailfs grants the ability for a peer to access tailfs shares. // PeerCapabilityTailFS grants the ability for a peer to access tailfs shares.
PeerCapabilityTailfs PeerCapability = "tailscale.com/cap/tailfs" PeerCapabilityTailFS PeerCapability = "tailscale.com/cap/tailfs"
) )
// NodeCapMap is a map of capabilities to their optional values. It is valid for // NodeCapMap is a map of capabilities to their optional values. It is valid for
@ -2211,8 +2211,8 @@ type Oauth2Token struct {
// tail end of an active direct connection in magicsock. // tail end of an active direct connection in magicsock.
NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime" NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime"
// NodeAttrsTailfsSharingEnabled enables sharing via Tailfs. // NodeAttrsTailFSSharingEnabled enables sharing via TailFS.
NodeAttrsTailfsSharingEnabled NodeCapability = "tailfs:share" NodeAttrsTailFSSharingEnabled NodeCapability = "tailfs:share"
) )
// SetDNSRequest is a request to add a DNS record. // SetDNSRequest is a request to add a DNS record.

View File

@ -15,6 +15,7 @@
"time" "time"
. "tailscale.com/tailcfg" . "tailscale.com/tailcfg"
"tailscale.com/tstest/deptest"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/opt" "tailscale.com/types/opt"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
@ -842,3 +843,14 @@ type rule struct {
}) })
} }
} }
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)
}

View File

@ -1,99 +1,37 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // 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 package tailfs
import ( import (
"log"
"net" "net"
"net/http" "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 { type Remote struct {
Name string Name string
URL string URL string
Available func() bool Available func() bool
} }
// NewFileSystemForLocal starts serving a filesystem for local clients. // FileSystemForLocal is the TailFS filesystem exposed to local clients. It
// Inbound connections must be handed to HandleConn. // provides a unified WebDAV interface to remote TailFS shares on other nodes.
func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal { type FileSystemForLocal interface {
if logf == nil { // HandleConn handles connections from local WebDAV clients
logf = log.Printf HandleConn(conn net.Conn, remoteAddr net.Addr) error
}
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 // SetRemotes sets the complete set of remotes on the given tailnet domain
// provides a unified WebDAV interface to remote Tailfs shares on other nodes. // using a map of name -> url. If transport is specified, that transport
type FileSystemForLocal struct { // will be used to connect to these remotes.
logf logger.Logf SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper)
cfs *compositefs.CompositeFileSystem
listener *connListener
}
func (s *FileSystemForLocal) startServing() { // Close() stops serving the WebDAV content
hs := &http.Server{ Close() error
Handler: &webdav.Handler{
FileSystem: s.cfs,
LockSystem: webdav.NewMemLS(),
},
}
go func() {
err := hs.Serve(s.listener)
if err != nil {
// TODO(oxtoacart): should we panic or something different here?
log.Printf("serve: %v", err)
}
}()
}
// HandleConn handles connections from local WebDAV clients
func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) error {
return s.listener.HandleConn(conn, remoteAddr)
}
// SetRemotes sets the complete set of remotes on the given tailnet domain
// using a map of name -> url. If transport is specified, that transport
// will be used to connect to these remotes.
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper) {
children := make([]*compositefs.Child, 0, len(remotes))
for _, remote := range remotes {
opts := webdavfs.Options{
URL: remote.URL,
Transport: transport,
StatCacheTTL: statCacheTTL,
Logf: s.logf,
}
children = append(children, &compositefs.Child{
Name: remote.Name,
FS: webdavfs.New(opts),
Available: remote.Available,
})
}
domainChild, found := s.cfs.GetChild(domain)
if !found {
domainChild = compositefs.New(compositefs.Options{Logf: s.logf})
s.cfs.SetChildren(&compositefs.Child{Name: domain, FS: domainChild})
}
domainChild.(*compositefs.CompositeFileSystem).SetChildren(children...)
}
// Close() stops serving the WebDAV content
func (s *FileSystemForLocal) Close() error {
s.cfs.Close()
return s.listener.Close()
} }

View File

@ -4,386 +4,57 @@
package tailfs package tailfs
import ( import (
"bufio"
"encoding/hex"
"fmt"
"log"
"math"
"net"
"net/http" "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 ( 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. // AllowShareAs reports whether sharing files as a specific user is allowed.
func AllowShareAs() bool { 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 { type Share struct {
// Name is how this share appears on remote nodes. // Name is how this share appears on remote nodes.
Name string `json:"name"` Name string `json:"name"`
// Path is the path to the directory on this machine that's being shared. // Path is the path to the directory on this machine that's being shared.
Path string `json:"path"` Path string `json:"path"`
// As is the UNIX or Windows username of the local account used for this // 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. // 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"` As string `json:"who"`
} }
func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote { // FileSystemForRemote is the TailFS filesystem exposed to remote nodes. It
if logf == nil {
logf = log.Printf
}
fs := &FileSystemForRemote{
logf: logf,
lockSystem: webdav.NewMemLS(),
fileSystems: make(map[string]webdav.FileSystem),
userServers: make(map[string]*userServer),
}
return fs
}
// FileSystemForRemote is the Tailfs filesystem exposed to remote nodes. It
// provides a unified WebDAV interface to local directories that have been // provides a unified WebDAV interface to local directories that have been
// shared. // shared.
type FileSystemForRemote struct { type FileSystemForRemote interface {
logf logger.Logf // SetFileServerAddr sets the address of the file server to which we
lockSystem webdav.LockSystem // 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)
// mu guards the below values. Acquire a write lock before updating any of // SetShares sets the complete set of shares exposed by this node. If
// them, acquire a read lock before reading any of them. // AllowShareAs() reports true, we will use one subprocess per user to
mu sync.RWMutex // access the filesystem (see userServer). Otherwise, we will use the file
fileServerAddr string // server configured via SetFileServerAddr.
shares map[string]*Share SetShares(shares map[string]*Share)
fileSystems map[string]webdav.FileSystem
userServers map[string]*userServer // ServeHTTPWithPerms behaves like the similar method from http.Handler but
} // also accepts a Permissions map that captures the permissions of the
// connecting node.
// SetFileServerAddr sets the address of the file server to which we ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request)
// should proxy. This is used on platforms like Windows and MacOS
// sandboxed where we can't spawn user-specific sub-processes and instead // Close() stops serving the WebDAV content
// rely on the UI application that's already running as an unprivileged Close() error
// 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,
} }

View File

@ -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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package tailfs package tailfsimpl
import ( import (
"net" "net"
@ -9,11 +9,11 @@
"sync" "sync"
"github.com/tailscale/xnet/webdav" "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. // 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. // serve up files as an unprivileged user.
type FileServer struct { type FileServer struct {
l net.Listener l net.Listener

View File

@ -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()
}

View File

@ -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,
}

View File

@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package tailfs package tailfsimpl
import ( import (
"context" "context"
@ -20,8 +20,9 @@
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/tailscale/xnet/webdav" "github.com/tailscale/xnet/webdav"
"tailscale.com/tailfs/shared" "tailscale.com/tailfs"
"tailscale.com/tailfs/webdavfs" "tailscale.com/tailfs/tailfsimpl/shared"
"tailscale.com/tailfs/tailfsimpl/webdavfs"
"tailscale.com/tstest" "tailscale.com/tstest"
) )
@ -38,10 +39,10 @@
func init() { func init() {
// set AllowShareAs() to false so that we don't try to use sub-processes // set AllowShareAs() to false so that we don't try to use sub-processes
// for access files on disk. // 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. // going over the Tailscale network stack.
func TestDirectoryListing(t *testing.T) { func TestDirectoryListing(t *testing.T) {
s := newSystem(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("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("domain should contain its only remote", shared.Join(domain), remote1)
s.checkDirList("remote with no shares should be empty", 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.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.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) 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() defer s.stop()
s.addRemote(remote1) 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.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
s.checkFileStatus(remote1, share11, file111) s.checkFileStatus(remote1, share11, file111)
s.checkFileContents(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 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) 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() defer s.stop()
s.addRemote(remote1) 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.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)) fi, err := s.fs.Stat(context.Background(), pathTo(remote1, share11, file111))
if err != nil { if err != nil {
@ -204,7 +205,7 @@ func TestFileRewind(t *testing.T) {
defer s.stop() defer s.stop()
s.addRemote(remote1) 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 // Create a file slightly longer than our max rewind buffer of 512
fileLength := webdavfs.MaxRewindBuffer + 1 fileLength := webdavfs.MaxRewindBuffer + 1
@ -267,7 +268,7 @@ type remote struct {
fs *FileSystemForRemote fs *FileSystemForRemote
fileServer *FileServer fileServer *FileServer
shares map[string]string shares map[string]string
permissions map[string]Permission permissions map[string]tailfs.Permission
mu sync.RWMutex mu sync.RWMutex
} }
@ -343,15 +344,15 @@ func (s *system) addRemote(name string) {
fileServer: fileServer, fileServer: fileServer,
fs: NewFileSystemForRemote(log.Printf), fs: NewFileSystemForRemote(log.Printf),
shares: make(map[string]string), shares: make(map[string]string),
permissions: make(map[string]Permission), permissions: make(map[string]tailfs.Permission),
} }
r.fs.SetFileServerAddr(fileServer.Addr()) r.fs.SetFileServerAddr(fileServer.Addr())
go http.Serve(l, r) go http.Serve(l, r)
s.remotes[name] = 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 { for name, r := range s.remotes {
remotes = append(remotes, &Remote{ remotes = append(remotes, &tailfs.Remote{
Name: name, Name: name,
URL: fmt.Sprintf("http://%s", r.l.Addr()), 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{}) 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] r, ok := s.remotes[remoteName]
if !ok { if !ok {
s.t.Fatalf("unknown remote %q", remoteName) 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.shares[shareName] = f
r.permissions[shareName] = permission 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 { for shareName, folder := range r.shares {
shares[shareName] = &Share{ shares[shareName] = &tailfs.Share{
Name: shareName, Name: shareName,
Path: folder, Path: folder,
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -530,7 +530,7 @@ func (s *Server) start() (reterr error) {
closePool.add(s.dialer) closePool.add(s.dialer)
sys.Set(eng) 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) ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
if err != nil { if err != nil {
return fmt.Errorf("netstack.Create: %w", err) return fmt.Errorf("netstack.Create: %w", err)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -133,7 +133,7 @@ type Impl struct {
ctxCancel context.CancelFunc // called on Close ctxCancel context.CancelFunc // called on Close
lb *ipnlocal.LocalBackend // or nil lb *ipnlocal.LocalBackend // or nil
dns *dns.Manager dns *dns.Manager
tailfsForLocal *tailfs.FileSystemForLocal // or nil tailFSForLocal tailfs.FileSystemForLocal // or nil
peerapiPort4Atomic atomic.Uint32 // uint16 port number for IPv4 peerapi peerapiPort4Atomic atomic.Uint32 // uint16 port number for IPv4 peerapi
peerapiPort6Atomic atomic.Uint32 // uint16 port number for IPv6 peerapi peerapiPort6Atomic atomic.Uint32 // uint16 port number for IPv6 peerapi
@ -161,7 +161,7 @@ type Impl struct {
const maxUDPPacketSize = tstun.MaxPacketSize const maxUDPPacketSize = tstun.MaxPacketSize
// Create creates and populates a new Impl. // 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 { if mc == nil {
return nil, errors.New("nil magicsock.Conn") 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, dialer: dialer,
connsOpenBySubnetIP: make(map[netip.Addr]int), connsOpenBySubnetIP: make(map[netip.Addr]int),
dns: dns, dns: dns,
tailfsForLocal: tailfsForLocal, tailFSForLocal: tailFSForLocal,
} }
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background()) ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc()) ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc())
@ -443,7 +443,7 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re
return filter.DropSilently 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. // care; resume processing.
if dst := p.Dst.Addr(); dst != serviceIP && dst != serviceIPv6 { if dst := p.Dst.Addr(); dst != serviceIP && dst != serviceIPv6 {
return filter.Accept return filter.Accept
@ -922,8 +922,8 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
// Local DNS Service (DNS and WebDAV) // Local DNS Service (DNS and WebDAV)
hittingServiceIP := dialIP == serviceIP || dialIP == serviceIPv6 hittingServiceIP := dialIP == serviceIP || dialIP == serviceIPv6
hittingDNS := hittingServiceIP && reqDetails.LocalPort == 53 hittingDNS := hittingServiceIP && reqDetails.LocalPort == 53
hittingTailfs := hittingServiceIP && ns.tailfsForLocal != nil && reqDetails.LocalPort == 8080 hittingTailFS := hittingServiceIP && ns.tailFSForLocal != nil && reqDetails.LocalPort == 8080
if hittingDNS || hittingTailfs { if hittingDNS || hittingTailFS {
c := getConnOrReset() c := getConnOrReset()
if c == nil { if c == nil {
return return
@ -931,8 +931,8 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
addrPort := netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort) addrPort := netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort)
if hittingDNS { if hittingDNS {
go ns.dns.HandleTCPConn(c, addrPort) go ns.dns.HandleTCPConn(c, addrPort)
} else if hittingTailfs { } else if hittingTailFS {
err := ns.tailfsForLocal.HandleConn(c, net.TCPAddrFromAddrPort(addrPort)) err := ns.tailFSForLocal.HandleConn(c, net.TCPAddrFromAddrPort(addrPort))
if err != nil { if err != nil {
ns.logf("netstack: tailfs.HandleConn: %v", err) ns.logf("netstack: tailfs.HandleConn: %v", err)
} }

View File

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