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>
pull/11095/head
Percy Wegmann 3 months ago committed by Percy Wegmann
parent 79b547804b
commit abab0d4197

@ -1418,25 +1418,25 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
return &cv, nil
}
// TailfsSetFileServerAddr instructs Tailfs to use the server at addr to access
// TailFSSetFileServerAddr instructs TailFS to use the server at addr to access
// the filesystem. This is used on platforms like Windows and MacOS to let
// Tailfs know to use the file server running in the GUI app.
func (lc *LocalClient) TailfsSetFileServerAddr(ctx context.Context, addr string) error {
// TailFS know to use the file server running in the GUI app.
func (lc *LocalClient) TailFSSetFileServerAddr(ctx context.Context, addr string) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
return err
}
// TailfsShareAdd adds the given share to the list of shares that Tailfs will
// TailFSShareAdd adds the given share to the list of shares that TailFS will
// serve to remote nodes. If a share with the same name already exists, the
// existing share is replaced/updated.
func (lc *LocalClient) TailfsShareAdd(ctx context.Context, share *tailfs.Share) error {
func (lc *LocalClient) TailFSShareAdd(ctx context.Context, share *tailfs.Share) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
return err
}
// TailfsShareRemove removes the share with the given name from the list of
// shares that Tailfs will serve to remote nodes.
func (lc *LocalClient) TailfsShareRemove(ctx context.Context, name string) error {
// TailFSShareRemove removes the share with the given name from the list of
// shares that TailFS will serve to remote nodes.
func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error {
_, err := lc.send(
ctx,
"DELETE",
@ -1448,9 +1448,9 @@ func (lc *LocalClient) TailfsShareRemove(ctx context.Context, name string) error
return err
}
// TailfsShareList returns the list of shares that Tailfs is currently serving
// TailFSShareList returns the list of shares that TailFS is currently serving
// to remote nodes.
func (lc *LocalClient) TailfsShareList(ctx context.Context) (map[string]*tailfs.Share, error) {
func (lc *LocalClient) TailFSShareList(ctx context.Context) (map[string]*tailfs.Share, error) {
result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
if err != nil {
return nil, err

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

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

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

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

@ -87,7 +87,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
💣 github.com/djherbis/times from tailscale.com/tailfs
💣 github.com/djherbis/times from tailscale.com/tailfs/tailfsimpl
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
@ -109,7 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
github.com/jellydator/ttlcache/v3 from tailscale.com/tailfs/webdavfs
github.com/jellydator/ttlcache/v3 from tailscale.com/tailfs/tailfsimpl/webdavfs
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
@ -155,7 +155,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tailscale/gowebdav from tailscale.com/tailfs/webdavfs
github.com/tailscale/gowebdav from tailscale.com/tailfs/tailfsimpl/webdavfs
github.com/tailscale/hujson from tailscale.com/ipn/conffile
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
@ -169,7 +169,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
github.com/tailscale/xnet/webdav from tailscale.com/tailfs+
github.com/tailscale/xnet/webdav from tailscale.com/tailfs/tailfsimpl+
github.com/tailscale/xnet/webdav/internal/xml from github.com/tailscale/xnet/webdav
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
@ -321,9 +321,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
tailscale.com/tailfs from tailscale.com/client/tailscale+
tailscale.com/tailfs/compositefs from tailscale.com/tailfs
tailscale.com/tailfs/shared from tailscale.com/tailfs/compositefs+
tailscale.com/tailfs/webdavfs from tailscale.com/tailfs
tailscale.com/tailfs/tailfsimpl from tailscale.com/cmd/tailscaled
tailscale.com/tailfs/tailfsimpl/compositefs from tailscale.com/tailfs/tailfsimpl
tailscale.com/tailfs/tailfsimpl/shared from tailscale.com/tailfs/tailfsimpl+
tailscale.com/tailfs/tailfsimpl/webdavfs from tailscale.com/tailfs/tailfsimpl
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock

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

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

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

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

@ -24,9 +24,9 @@ import (
)
const (
// TailfsLocalPort is the port on which the Tailfs listens for location
// TailFSLocalPort is the port on which the TailFS listens for location
// connections on quad 100.
TailfsLocalPort = 8080
TailFSLocalPort = 8080
tailfsSharesStateKey = ipn.StateKey("_tailfs-shares")
)
@ -36,27 +36,25 @@ var (
errInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
)
// TailfsSharingEnabled reports whether sharing to remote nodes via tailfs is
// TailFSSharingEnabled reports whether sharing to remote nodes via tailfs is
// enabled. This is currently based on checking for the tailfs:share node
// attribute.
func (b *LocalBackend) TailfsSharingEnabled() bool {
func (b *LocalBackend) TailFSSharingEnabled() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.tailfsSharingEnabledLocked()
return b.tailFSSharingEnabledLocked()
}
func (b *LocalBackend) tailfsSharingEnabledLocked() bool {
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailfsSharingEnabled)
func (b *LocalBackend) tailFSSharingEnabledLocked() bool {
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailFSSharingEnabled)
}
// TailfsSetFileServerAddr tells tailfs to use the given address for connecting
// TailFSSetFileServerAddr tells tailfs to use the given address for connecting
// to the tailfs.FileServer that's exposing local files as an unprivileged
// user.
func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error {
b.mu.Lock()
fs := b.tailfsForRemote
b.mu.Unlock()
if fs == nil {
func (b *LocalBackend) TailFSSetFileServerAddr(addr string) error {
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return errors.New("tailfs not enabled")
}
@ -64,11 +62,11 @@ func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error {
return nil
}
// TailfsAddShare adds the given share if no share with that name exists, or
// TailFSAddShare adds the given share if no share with that name exists, or
// replaces the existing share if one with the same name already exists.
// To avoid potential incompatibilities across file systems, share names are
// limited to alphanumeric characters and the underscore _.
func (b *LocalBackend) TailfsAddShare(share *tailfs.Share) error {
func (b *LocalBackend) TailFSAddShare(share *tailfs.Share) error {
var err error
share.Name, err = normalizeShareName(share.Name)
if err != nil {
@ -104,11 +102,12 @@ func normalizeShareName(name string) (string, error) {
}
func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]string, error) {
if b.tailfsForRemote == nil {
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return nil, errors.New("tailfs not enabled")
}
shares, err := b.tailfsGetSharesLocked()
shares, err := b.tailFSGetSharesLocked()
if err != nil {
return nil, err
}
@ -121,17 +120,21 @@ func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]str
if err != nil {
return nil, fmt.Errorf("write state: %w", err)
}
b.tailfsForRemote.SetShares(shares)
fs.SetShares(shares)
return shareNameMap(shares), nil
}
// TailfsRemoveShare removes the named share. Share names are forced to
// TailFSRemoveShare removes the named share. Share names are forced to
// lowercase.
func (b *LocalBackend) TailfsRemoveShare(name string) error {
func (b *LocalBackend) TailFSRemoveShare(name string) error {
// Force all share names to lowercase to avoid potential incompatibilities
// with clients that don't support case-sensitive filenames.
name = strings.ToLower(name)
var err error
name, err = normalizeShareName(name)
if err != nil {
return err
}
b.mu.Lock()
shares, err := b.tailfsRemoveShareLocked(name)
@ -145,11 +148,12 @@ func (b *LocalBackend) TailfsRemoveShare(name string) error {
}
func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string, error) {
if b.tailfsForRemote == nil {
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return nil, errors.New("tailfs not enabled")
}
shares, err := b.tailfsGetSharesLocked()
shares, err := b.tailFSGetSharesLocked()
if err != nil {
return nil, err
}
@ -166,7 +170,7 @@ func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string,
if err != nil {
return nil, fmt.Errorf("write state: %w", err)
}
b.tailfsForRemote.SetShares(shares)
fs.SetShares(shares)
return shareNameMap(shares), nil
}
@ -182,13 +186,13 @@ func shareNameMap(sharesByName map[string]*tailfs.Share) map[string]string {
// tailfsNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
// about the latest set of shares, supplied as a map of name -> directory.
func (b *LocalBackend) tailfsNotifyShares(shares map[string]string) {
b.send(ipn.Notify{TailfsShares: shares})
b.send(ipn.Notify{TailFSShares: shares})
}
// tailfsNotifyCurrentSharesLocked sends an ipn.Notify with the current set of
// tailfs shares.
func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() {
shares, err := b.tailfsGetSharesLocked()
// tailFSNotifyCurrentSharesLocked sends an ipn.Notify with the current set of
// TailFS shares.
func (b *LocalBackend) tailFSNotifyCurrentSharesLocked() {
shares, err := b.tailFSGetSharesLocked()
if err != nil {
b.logf("error notifying current tailfs shares: %v", err)
return
@ -197,15 +201,16 @@ func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() {
go b.tailfsNotifyShares(shareNameMap(shares))
}
// TailfsGetShares() returns the current set of shares from the state store.
func (b *LocalBackend) TailfsGetShares() (map[string]*tailfs.Share, error) {
// TailFSGetShares returns the current set of shares from the state store,
// stored under ipn.StateKey("_tailfs-shares").
func (b *LocalBackend) TailFSGetShares() (map[string]*tailfs.Share, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.tailfsGetSharesLocked()
return b.tailFSGetSharesLocked()
}
func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error) {
func (b *LocalBackend) tailFSGetSharesLocked() (map[string]*tailfs.Share, error) {
data, err := b.store.ReadState(tailfsSharesStateKey)
if err != nil {
if errors.Is(err, ipn.ErrStateNotExist) {
@ -223,27 +228,27 @@ func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error)
return shares, nil
}
// updateTailfsListenersLocked creates listeners on the local Tailfs port.
// updateTailFSListenersLocked creates listeners on the local TailFS port.
// This is needed to properly route local traffic when using kernel networking
// mode.
func (b *LocalBackend) updateTailfsListenersLocked() {
func (b *LocalBackend) updateTailFSListenersLocked() {
if b.netMap == nil {
return
}
addrs := b.netMap.GetAddresses()
oldListeners := b.tailfsListeners
oldListeners := b.tailFSListeners
newListeners := make(map[netip.AddrPort]*localListener, addrs.Len())
for i := range addrs.LenIter() {
if fs, ok := b.sys.TailfsForLocal.GetOK(); ok {
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), TailfsLocalPort)
if sl, ok := b.tailfsListeners[addrPort]; ok {
if fs, ok := b.sys.TailFSForLocal.GetOK(); ok {
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), TailFSLocalPort)
if sl, ok := b.tailFSListeners[addrPort]; ok {
newListeners[addrPort] = sl
delete(oldListeners, addrPort)
continue // already listening
}
sl := b.newTailfsListener(context.Background(), fs, addrPort, b.logf)
sl := b.newTailFSListener(context.Background(), fs, addrPort, b.logf)
newListeners[addrPort] = sl
go sl.Run()
}
@ -255,9 +260,9 @@ func (b *LocalBackend) updateTailfsListenersLocked() {
}
}
// newTailfsListener returns a listener for local connections to a tailfs
// newTailFSListener returns a listener for local connections to a tailfs
// WebDAV FileSystem.
func (b *LocalBackend) newTailfsListener(ctx context.Context, fs *tailfs.FileSystemForLocal, ap netip.AddrPort, logf logger.Logf) *localListener {
func (b *LocalBackend) newTailFSListener(ctx context.Context, fs tailfs.FileSystemForLocal, ap netip.AddrPort, logf logger.Logf) *localListener {
ctx, cancel := context.WithCancel(ctx)
return &localListener{
b: b,
@ -273,10 +278,10 @@ func (b *LocalBackend) newTailfsListener(ctx context.Context, fs *tailfs.FileSys
}
}
// updateTailfsPeersLocked sets all applicable peers from the netmap as tailfs
// updateTailFSPeersLocked sets all applicable peers from the netmap as tailfs
// remotes.
func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
fs, ok := b.sys.TailfsForLocal.GetOK()
func (b *LocalBackend) updateTailFSPeersLocked(nm *netmap.NetworkMap) {
fs, ok := b.sys.TailFSForLocal.GetOK()
if !ok {
return
}
@ -284,7 +289,7 @@ func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
tailfsRemotes := make([]*tailfs.Remote, 0, len(nm.Peers))
for _, p := range nm.Peers {
peerID := p.ID()
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailfsPrefix[1:])
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailFSPrefix[1:])
tailfsRemotes = append(tailfsRemotes, &tailfs.Remote{
Name: p.DisplayName(false),
URL: url,
@ -314,5 +319,5 @@ func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
},
})
}
fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailfsTransport{b: b})
fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailFSTransport{b: b})
}

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

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

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

@ -1,99 +1,37 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tailfs provides a filesystem that allows sharing folders between
// Tailscale nodes using WebDAV. The actual implementation of the core TailFS
// functionality lives in package tailfsimpl. These packages are separated to
// allow users of tailfs to refer to the interfaces without having a hard
// dependency on tailfs, so that programs which don't actually use tailfs can
// avoid its transitive dependencies.
package tailfs
import (
"log"
"net"
"net/http"
"github.com/tailscale/xnet/webdav"
"tailscale.com/tailfs/compositefs"
"tailscale.com/tailfs/webdavfs"
"tailscale.com/types/logger"
)
// Remote represents a remote Tailfs node.
// Remote represents a remote TailFS node.
type Remote struct {
Name string
URL string
Available func() bool
}
// NewFileSystemForLocal starts serving a filesystem for local clients.
// Inbound connections must be handed to HandleConn.
func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
if logf == nil {
logf = log.Printf
}
fs := &FileSystemForLocal{
logf: logf,
cfs: compositefs.New(compositefs.Options{Logf: logf}),
listener: newConnListener(),
}
fs.startServing()
return fs
}
// FileSystemForLocal is the Tailfs filesystem exposed to local clients. It
// provides a unified WebDAV interface to remote Tailfs shares on other nodes.
type FileSystemForLocal struct {
logf logger.Logf
cfs *compositefs.CompositeFileSystem
listener *connListener
}
func (s *FileSystemForLocal) startServing() {
hs := &http.Server{
Handler: &webdav.Handler{
FileSystem: s.cfs,
LockSystem: webdav.NewMemLS(),
},
}
go func() {
err := hs.Serve(s.listener)
if err != nil {
// TODO(oxtoacart): should we panic or something different here?
log.Printf("serve: %v", err)
}
}()
}
// HandleConn handles connections from local WebDAV clients
func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) error {
return s.listener.HandleConn(conn, remoteAddr)
}
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
type FileSystemForLocal interface {
// HandleConn handles connections from local WebDAV clients
HandleConn(conn net.Conn, remoteAddr net.Addr) error
// SetRemotes sets the complete set of remotes on the given tailnet domain
// using a map of name -> url. If transport is specified, that transport
// will be used to connect to these remotes.
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper) {
children := make([]*compositefs.Child, 0, len(remotes))
for _, remote := range remotes {
opts := webdavfs.Options{
URL: remote.URL,
Transport: transport,
StatCacheTTL: statCacheTTL,
Logf: s.logf,
}
children = append(children, &compositefs.Child{
Name: remote.Name,
FS: webdavfs.New(opts),
Available: remote.Available,
})
}
domainChild, found := s.cfs.GetChild(domain)
if !found {
domainChild = compositefs.New(compositefs.Options{Logf: s.logf})
s.cfs.SetChildren(&compositefs.Child{Name: domain, FS: domainChild})
}
domainChild.(*compositefs.CompositeFileSystem).SetChildren(children...)
}
// SetRemotes sets the complete set of remotes on the given tailnet domain
// using a map of name -> url. If transport is specified, that transport
// will be used to connect to these remotes.
SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper)
// Close() stops serving the WebDAV content
func (s *FileSystemForLocal) Close() error {
s.cfs.Close()
return s.listener.Close()
// Close() stops serving the WebDAV content
Close() error
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailfs
package tailfsimpl
import (
"net"
@ -9,11 +9,11 @@ import (
"sync"
"github.com/tailscale/xnet/webdav"
"tailscale.com/tailfs/shared"
"tailscale.com/tailfs/tailfsimpl/shared"
)
// FileServer is a standalone WebDAV server that dynamically serves up shares.
// It's typically used in a separate process from the actual Tailfs server to
// It's typically used in a separate process from the actual TailFS server to
// serve up files as an unprivileged user.
type FileServer struct {
l net.Listener

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save