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:
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 interface {
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
HandleConn(conn net.Conn, remoteAddr net.Addr) error
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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)
|
||||
|
||||
func (s *FileSystemForLocal) startServing() {
|
||||
hs := &http.Server{
|
||||
Handler: &webdav.Handler{
|
||||
FileSystem: s.cfs,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
err := hs.Serve(s.listener)
|
||||
if err != nil {
|
||||
// TODO(oxtoacart): should we panic or something different here?
|
||||
log.Printf("serve: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) error {
|
||||
return s.listener.HandleConn(conn, remoteAddr)
|
||||
}
|
||||
|
||||
// SetRemotes sets the complete set of remotes on the given tailnet domain
|
||||
// using a map of name -> url. If transport is specified, that transport
|
||||
// will be used to connect to these remotes.
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper) {
|
||||
children := make([]*compositefs.Child, 0, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
opts := webdavfs.Options{
|
||||
URL: remote.URL,
|
||||
Transport: transport,
|
||||
StatCacheTTL: statCacheTTL,
|
||||
Logf: s.logf,
|
||||
}
|
||||
children = append(children, &compositefs.Child{
|
||||
Name: remote.Name,
|
||||
FS: webdavfs.New(opts),
|
||||
Available: remote.Available,
|
||||
})
|
||||
}
|
||||
|
||||
domainChild, found := s.cfs.GetChild(domain)
|
||||
if !found {
|
||||
domainChild = compositefs.New(compositefs.Options{Logf: s.logf})
|
||||
s.cfs.SetChildren(&compositefs.Child{Name: domain, FS: domainChild})
|
||||
}
|
||||
domainChild.(*compositefs.CompositeFileSystem).SetChildren(children...)
|
||||
}
|
||||
|
||||
// Close() stops serving the WebDAV content
|
||||
func (s *FileSystemForLocal) Close() error {
|
||||
s.cfs.Close()
|
||||
return s.listener.Close()
|
||||
// Close() stops serving the WebDAV content
|
||||
Close() error
|
||||
}
|
||||
|
|
389
tailfs/remote.go
389
tailfs/remote.go
|
@ -4,386 +4,57 @@
|
|||
package tailfs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailfs/compositefs"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/webdavfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
disallowShareAs = false
|
||||
// DisallowShareAs forcibly disables sharing as a specific user, only used
|
||||
// for testing.
|
||||
DisallowShareAs = false
|
||||
)
|
||||
|
||||
// AllowShareAs reports whether sharing files as a specific user is allowed.
|
||||
func AllowShareAs() bool {
|
||||
return !disallowShareAs && doAllowShareAs()
|
||||
return !DisallowShareAs && doAllowShareAs()
|
||||
}
|
||||
|
||||
// Share represents a folder that's shared with remote Tailfs nodes.
|
||||
// Share configures a folder to be shared through TailFS.
|
||||
type Share struct {
|
||||
// Name is how this share appears on remote nodes.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the directory on this machine that's being shared.
|
||||
Path string `json:"path"`
|
||||
|
||||
// As is the UNIX or Windows username of the local account used for this
|
||||
// share. File read/write permissions are enforced based on this username.
|
||||
// Can be left blank to use the default value of "whoever is running the
|
||||
// Tailscale GUI".
|
||||
As string `json:"who"`
|
||||
}
|
||||
|
||||
func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
fs := &FileSystemForRemote{
|
||||
logf: logf,
|
||||
lockSystem: webdav.NewMemLS(),
|
||||
fileSystems: make(map[string]webdav.FileSystem),
|
||||
userServers: make(map[string]*userServer),
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForRemote is the Tailfs filesystem exposed to remote nodes. It
|
||||
// FileSystemForRemote is the TailFS filesystem exposed to remote nodes. It
|
||||
// provides a unified WebDAV interface to local directories that have been
|
||||
// shared.
|
||||
type FileSystemForRemote struct {
|
||||
logf logger.Logf
|
||||
lockSystem webdav.LockSystem
|
||||
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)
|
||||
|
||||
// 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,
|
||||
// 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 {
|
29
tsd/tsd.go
29
tsd/tsd.go
|
@ -38,17 +38,18 @@ import (
|
|||
|
||||
// System contains all the subsystems of a Tailscale node (tailscaled, etc.)
|
||||
type System struct {
|
||||
Dialer SubSystem[*tsdial.Dialer]
|
||||
DNSManager SubSystem[*dns.Manager] // can get its *resolver.Resolver from DNSManager.Resolver
|
||||
Engine SubSystem[wgengine.Engine]
|
||||
NetMon SubSystem[*netmon.Monitor]
|
||||
MagicSock SubSystem[*magicsock.Conn]
|
||||
NetstackRouter SubSystem[bool] // using Netstack at all (either entirely or at least for subnets)
|
||||
Router SubSystem[router.Router]
|
||||
Tun SubSystem[*tstun.Wrapper]
|
||||
StateStore SubSystem[ipn.StateStore]
|
||||
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
|
||||
TailfsForLocal SubSystem[*tailfs.FileSystemForLocal]
|
||||
Dialer SubSystem[*tsdial.Dialer]
|
||||
DNSManager SubSystem[*dns.Manager] // can get its *resolver.Resolver from DNSManager.Resolver
|
||||
Engine SubSystem[wgengine.Engine]
|
||||
NetMon SubSystem[*netmon.Monitor]
|
||||
MagicSock SubSystem[*magicsock.Conn]
|
||||
NetstackRouter SubSystem[bool] // using Netstack at all (either entirely or at least for subnets)
|
||||
Router SubSystem[router.Router]
|
||||
Tun SubSystem[*tstun.Wrapper]
|
||||
StateStore SubSystem[ipn.StateStore]
|
||||
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
|
||||
TailFSForLocal SubSystem[tailfs.FileSystemForLocal]
|
||||
TailFSForRemote SubSystem[tailfs.FileSystemForRemote]
|
||||
|
||||
// InitialConfig is initial server config, if any.
|
||||
// It is nil if the node is not in declarative mode.
|
||||
|
@ -100,8 +101,10 @@ func (s *System) Set(v any) {
|
|||
s.StateStore.Set(v)
|
||||
case NetstackImpl:
|
||||
s.Netstack.Set(v)
|
||||
case *tailfs.FileSystemForLocal:
|
||||
s.TailfsForLocal.Set(v)
|
||||
case tailfs.FileSystemForLocal:
|
||||
s.TailFSForLocal.Set(v)
|
||||
case tailfs.FileSystemForRemote:
|
||||
s.TailFSForRemote.Set(v)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %T", v))
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue