summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorPercy Wegmann <percy@tailscale.com>2024-02-24 12:20:41 -0600
committerPercy Wegmann <percy@tailscale.com>2024-02-24 17:32:45 -0600
commit398594743711a8c8b63a708a0f4ad98c5e5f2efb (patch)
tree9ef04a3a5269053d5802466b00f7105127a6f9f6
parent15b2c674bff3cfc17d1d77c09105bf818428a238 (diff)
downloadtailscale-oxtoacart/automount.tar.xz
tailscale-oxtoacart/automount.zip
cmd/tailscale,ipn:add support for automounting TailFS shares on MacOSoxtoacart/automount
This adds two flags to the "tailscale set" command. --automount-enabled enables automatically mounting TailFS shares --automount-path optionally specifies the path at which to automount If --automount-path is not set, TailFS will be mounted at /Volumes/tailscale. The mount is owned by whatever user invoked "tailscale set" and has mode 0700 set (read,write,execute only by owning user). By default, automounting is not enabled. Updates tailscale/corp#16827 Signed-off-by: Percy Wegmann <percy@tailscale.com>
-rw-r--r--cmd/derper/depaware.txt4
-rw-r--r--cmd/tailscale/cli/set.go16
-rw-r--r--cmd/tailscale/cli/up.go2
-rw-r--r--ipn/ipn_clone.go1
-rw-r--r--ipn/ipn_view.go2
-rw-r--r--ipn/ipnlocal/local.go3
-rw-r--r--ipn/ipnlocal/tailfs.go22
-rw-r--r--ipn/localapi/localapi.go9
-rw-r--r--ipn/prefs.go48
-rw-r--r--ipn/prefs_test.go75
-rw-r--r--tailfs/automount.go12
-rw-r--r--tailfs/automount_darwin.go62
-rw-r--r--tailfs/automount_nondarwin.go20
13 files changed, 253 insertions, 23 deletions
diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt
index fb83b65f5..21030ef05 100644
--- a/cmd/derper/depaware.txt
+++ b/cmd/derper/depaware.txt
@@ -114,7 +114,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 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 from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/derp+
@@ -264,7 +264,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/derper
- W os/user from tailscale.com/util/winutil
+ DW os/user from tailscale.com/util/winutil+
path from github.com/prometheus/client_golang/prometheus/internal+
path/filepath from crypto/x509+
reflect from crypto/x509+
diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go
index 02d4f5a06..68e8e1dcc 100644
--- a/cmd/tailscale/cli/set.go
+++ b/cmd/tailscale/cli/set.go
@@ -18,6 +18,7 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/net/tsaddr"
"tailscale.com/safesocket"
+ "tailscale.com/tailfs"
"tailscale.com/types/opt"
"tailscale.com/types/views"
"tailscale.com/version"
@@ -56,6 +57,8 @@ type setArgsT struct {
updateCheck bool
updateApply bool
postureChecking bool
+ automountEnabled bool
+ automountPath string
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -76,6 +79,12 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "run a web interface for managing this node, served over Tailscale at port 5252")
+ automountDisclaimer := ""
+ if !tailfs.AutomountSupported() {
+ automountDisclaimer = "(NOT AVAILABLE ON THIS SYSTEM) "
+ }
+ setf.BoolVar(&setArgs.automountEnabled, "automount-enabled", false, fmt.Sprintf("%sautomatically mount Tailscale shares", automountDisclaimer))
+ setf.StringVar(&setArgs.automountPath, "automount-path", "", fmt.Sprintf(`%spath at which to automount shares, leave blank to default to %q`, automountDisclaimer, tailfs.DefaultAutomountPath()))
if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
@@ -124,6 +133,10 @@ func runSet(ctx context.Context, args []string) (retErr error) {
Advertise: setArgs.advertiseConnector,
},
PostureChecking: setArgs.postureChecking,
+ AutomountShares: ipn.AutomountPrefs{
+ Enabled: setArgs.automountEnabled,
+ Path: setArgs.automountPath,
+ },
},
}
@@ -151,6 +164,9 @@ func runSet(ctx context.Context, args []string) (retErr error) {
if maskedPrefs.IsEmpty() {
return flag.ErrHelp
}
+ if maskedPrefs.AutomountSharesSet && !tailfs.AutomountSupported() {
+ return errors.New("flag automount-enabled is not supported on this system")
+ }
curPrefs, err := localClient.GetPrefs(ctx)
if err != nil {
diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go
index 3aa66865f..7695389d4 100644
--- a/cmd/tailscale/cli/up.go
+++ b/cmd/tailscale/cli/up.go
@@ -722,6 +722,8 @@ func init() {
addPrefFlagMapping("auto-update", "AutoUpdate.Apply")
addPrefFlagMapping("advertise-connector", "AppConnector")
addPrefFlagMapping("posture-checking", "PostureChecking")
+ addPrefFlagMapping("automount-enabled", "AutomountShares")
+ addPrefFlagMapping("automount-path", "AutomountShares")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {
diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go
index 40cc44296..8ac5d95d3 100644
--- a/ipn/ipn_clone.go
+++ b/ipn/ipn_clone.go
@@ -56,6 +56,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
AppConnector AppConnectorPrefs
PostureChecking bool
NetfilterKind string
+ AutomountShares AutomountPrefs
Persist *persist.Persist
}{})
diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go
index 18436867d..e661c35c6 100644
--- a/ipn/ipn_view.go
+++ b/ipn/ipn_view.go
@@ -91,6 +91,7 @@ func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpda
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
+func (v PrefsView) AutomountShares() AutomountPrefs { return v.ж.AutomountShares }
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@@ -121,6 +122,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
AppConnector AppConnectorPrefs
PostureChecking bool
NetfilterKind string
+ AutomountShares AutomountPrefs
Persist *persist.Persist
}{})
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index b8aa769a1..5d6175e7c 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -1847,6 +1847,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// use logout instead.
cc.Login(nil, controlclient.LoginDefault)
}
+
+ b.tailFSConfigureAutomount(ipn.AutomountPrefs{}, prefs.AutomountShares())
b.stateMachine()
return nil
}
@@ -3261,6 +3263,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
b.authReconfig()
}
+ b.tailFSConfigureAutomount(oldp.AutomountShares(), prefs.AutomountShares())
b.send(ipn.Notify{Prefs: &prefs})
return prefs
}
diff --git a/ipn/ipnlocal/tailfs.go b/ipn/ipnlocal/tailfs.go
index d06599661..d52a3b789 100644
--- a/ipn/ipnlocal/tailfs.go
+++ b/ipn/ipnlocal/tailfs.go
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"os"
+ "path/filepath"
"regexp"
"strings"
@@ -278,3 +279,24 @@ func (b *LocalBackend) updateTailFSPeersLocked(nm *netmap.NetworkMap) {
}
fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailFSTransport{b: b})
}
+
+func (b *LocalBackend) tailFSConfigureAutomount(old, new ipn.AutomountPrefs) {
+ oldPath := filepath.Clean(old.PathOrDefault())
+ settingsChanged := !new.Equals(old)
+
+ if old.Enabled && pathExists(oldPath) && settingsChanged {
+ b.logf("Unmounting shares from %q", oldPath)
+ tailfs.UnmountShares(oldPath)
+ }
+
+ newPath := filepath.Clean(new.PathOrDefault())
+ if new.Enabled && !pathExists(newPath) {
+ b.logf("Mounting shares at %q as %q", newPath, new.AsUser)
+ tailfs.MountShares(newPath, new.AsUser)
+ }
+}
+
+func pathExists(p string) bool {
+ _, err := os.Stat(p)
+ return err == nil
+}
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index a1b7da46f..df9809f54 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -1374,6 +1374,15 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
+ if mp.AutomountSharesSet {
+ // Set AutomountShares user to the connecting username.
+ var err error
+ mp.AutomountShares.AsUser, err = h.getUsername()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
var err error
prefs, err = h.b.EditPrefs(mp)
if err != nil {
diff --git a/ipn/prefs.go b/ipn/prefs.go
index 7bfbd613f..b03de4430 100644
--- a/ipn/prefs.go
+++ b/ipn/prefs.go
@@ -21,6 +21,7 @@ import (
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
+ "tailscale.com/tailfs"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
@@ -222,6 +223,10 @@ type Prefs struct {
// Linux-only.
NetfilterKind string
+ // AutomountShares configures automatic mounting of the TailFS file system
+ // at a local path.
+ AutomountShares AutomountPrefs
+
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
@@ -259,6 +264,34 @@ type AppConnectorPrefs struct {
Advertise bool
}
+// AutomountPrefs are the settings for automounting TailFS shares.
+type AutomountPrefs struct {
+ // Enabled specifies whether or not automounting is enabled.
+ Enabled bool
+
+ // The path at which we mount. If blank, we default to an os-specific
+ // location like /Volumes/tailscale.
+ Path string
+
+ // AsUser specifies the user who will own the mounted folder.
+ AsUser string
+}
+
+// PathOrDefault returns the configured Path or the os-specific
+// [tailfs.DefaultAutomountPath] if no Path was specified.
+func (am AutomountPrefs) PathOrDefault() string {
+ if am.Path != "" {
+ return am.Path
+ }
+ return tailfs.DefaultAutomountPath()
+}
+
+func (am1 AutomountPrefs) Equals(am2 AutomountPrefs) bool {
+ return am1.Enabled == am2.Enabled &&
+ am1.Path == am2.Path &&
+ am1.AsUser == am2.AsUser
+}
+
// MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
//
// Each FooSet field maps to a corresponding Foo field in Prefs. FooSet can be
@@ -293,6 +326,7 @@ type MaskedPrefs struct {
AppConnectorSet bool `json:",omitempty"`
PostureCheckingSet bool `json:",omitempty"`
NetfilterKindSet bool `json:",omitempty"`
+ AutomountSharesSet bool `json:",omitempty"`
}
type AutoUpdatePrefsMask struct {
@@ -498,6 +532,7 @@ func (p *Prefs) pretty(goos string) string {
}
sb.WriteString(p.AutoUpdate.Pretty())
sb.WriteString(p.AppConnector.Pretty())
+ sb.WriteString(p.AutomountShares.Pretty())
if p.Persist != nil {
sb.WriteString(p.Persist.Pretty())
} else {
@@ -556,7 +591,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.AutoUpdate.Equals(p2.AutoUpdate) &&
p.AppConnector == p2.AppConnector &&
p.PostureChecking == p2.PostureChecking &&
- p.NetfilterKind == p2.NetfilterKind
+ p.NetfilterKind == p2.NetfilterKind &&
+ p.AutomountShares.Equals(p2.AutomountShares)
}
func (au AutoUpdatePrefs) Pretty() string {
@@ -576,6 +612,16 @@ func (ap AppConnectorPrefs) Pretty() string {
return ""
}
+func (am AutomountPrefs) Pretty() string {
+ if !am.Enabled {
+ return "automount=off "
+ }
+ if am.Path != "" {
+ return fmt.Sprintf("automount=%s ", am.Path)
+ }
+ return "automount=on "
+}
+
func compareIPNets(a, b []netip.Prefix) bool {
if len(a) != len(b) {
return false
diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go
index 9251bb2bb..3fe4e2fce 100644
--- a/ipn/prefs_test.go
+++ b/ipn/prefs_test.go
@@ -62,6 +62,7 @@ func TestPrefsEqual(t *testing.T) {
"AppConnector",
"PostureChecking",
"NetfilterKind",
+ "AutomountShares",
"Persist",
}
if have := fieldsOf(reflect.TypeFor[Prefs]()); !reflect.DeepEqual(have, prefsHandles) {
@@ -339,6 +340,26 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{NetfilterKind: ""},
false,
},
+ {
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path", AsUser: "username"}},
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path", AsUser: "username"}},
+ true,
+ },
+ {
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path", AsUser: "username"}},
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: false, Path: "path", AsUser: "username"}},
+ false,
+ },
+ {
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path", AsUser: "username"}},
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path2", AsUser: "username"}},
+ false,
+ },
+ {
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path", AsUser: "username"}},
+ &Prefs{AutomountShares: AutomountPrefs{Enabled: true, Path: "path", AsUser: "username2"}},
+ false,
+ },
}
for i, tt := range tests {
got := tt.a.Equals(tt.b)
@@ -423,22 +444,22 @@ func TestPrefsPretty(t *testing.T) {
{
Prefs{},
"linux",
- "Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}",
+ "Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=off Persist=nil}",
},
{
Prefs{},
"windows",
- "Prefs{ra=false mesh=false dns=false want=false update=off Persist=nil}",
+ "Prefs{ra=false mesh=false dns=false want=false update=off automount=off Persist=nil}",
},
{
Prefs{ShieldsUp: true},
"windows",
- "Prefs{ra=false mesh=false dns=false want=false shields=true update=off Persist=nil}",
+ "Prefs{ra=false mesh=false dns=false want=false shields=true update=off automount=off Persist=nil}",
},
{
Prefs{AllowSingleHosts: true},
"windows",
- "Prefs{ra=false dns=false want=false update=off Persist=nil}",
+ "Prefs{ra=false dns=false want=false update=off automount=off Persist=nil}",
},
{
Prefs{
@@ -446,7 +467,7 @@ func TestPrefsPretty(t *testing.T) {
AllowSingleHosts: true,
},
"windows",
- "Prefs{ra=false dns=false want=false notepad=true update=off Persist=nil}",
+ "Prefs{ra=false dns=false want=false notepad=true update=off automount=off Persist=nil}",
},
{
Prefs{
@@ -455,7 +476,7 @@ func TestPrefsPretty(t *testing.T) {
ForceDaemon: true, // server mode
},
"windows",
- "Prefs{ra=false dns=false want=true server=true update=off Persist=nil}",
+ "Prefs{ra=false dns=false want=true server=true update=off automount=off Persist=nil}",
},
{
Prefs{
@@ -465,14 +486,14 @@ func TestPrefsPretty(t *testing.T) {
AdvertiseTags: []string{"tag:foo", "tag:bar"},
},
"darwin",
- `Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" update=off Persist=nil}`,
+ `Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" update=off automount=off Persist=nil}`,
},
{
Prefs{
Persist: &persist.Persist{},
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n= u=""}}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=off Persist{lm=, o=, n= u=""}}`,
},
{
Prefs{
@@ -481,21 +502,21 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
},
{
Prefs{
ExitNodeIP: netip.MustParseAddr("1.2.3.4"),
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off update=off automount=off Persist=nil}`,
},
{
Prefs{
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off update=off automount=off Persist=nil}`,
},
{
Prefs{
@@ -503,21 +524,21 @@ func TestPrefsPretty(t *testing.T) {
ExitNodeAllowLANAccess: true,
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off update=off automount=off Persist=nil}`,
},
{
Prefs{
ExitNodeAllowLANAccess: true,
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=off Persist=nil}`,
},
{
Prefs{
Hostname: "foo",
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" update=off automount=off Persist=nil}`,
},
{
Prefs{
@@ -527,7 +548,7 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=check Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=check automount=off Persist=nil}`,
},
{
Prefs{
@@ -537,7 +558,7 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=on Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=on automount=off Persist=nil}`,
},
{
Prefs{
@@ -546,7 +567,7 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off appconnector=advertise Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off appconnector=advertise automount=off Persist=nil}`,
},
{
Prefs{
@@ -555,21 +576,35 @@ func TestPrefsPretty(t *testing.T) {
},
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=off Persist=nil}`,
},
{
Prefs{
NetfilterKind: "iptables",
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off netfilterKind=iptables update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off netfilterKind=iptables update=off automount=off Persist=nil}`,
},
{
Prefs{
NetfilterKind: "",
},
"linux",
- `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=off Persist=nil}`,
+ },
+ {
+ Prefs{
+ AutomountShares: AutomountPrefs{Enabled: true},
+ },
+ "linux",
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=on Persist=nil}`,
+ },
+ {
+ Prefs{
+ AutomountShares: AutomountPrefs{Enabled: true, Path: "/some/path"},
+ },
+ "linux",
+ `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off automount=/some/path Persist=nil}`,
},
}
for i, tt := range tests {
diff --git a/tailfs/automount.go b/tailfs/automount.go
new file mode 100644
index 000000000..b03bfd279
--- /dev/null
+++ b/tailfs/automount.go
@@ -0,0 +1,12 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package tailfs
+
+import "tailscale.com/version"
+
+// AutomountSupported reports whether TailFS automounting is supported on this
+// system.
+func AutomountSupported() bool {
+ return DefaultAutomountPath() != "" && !version.IsSandboxedMacOS()
+}
diff --git a/tailfs/automount_darwin.go b/tailfs/automount_darwin.go
new file mode 100644
index 000000000..2960ad7cf
--- /dev/null
+++ b/tailfs/automount_darwin.go
@@ -0,0 +1,62 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build darwin
+
+package tailfs
+
+import (
+ "log"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+)
+
+// DefaultAutomountPath returns the default automount path. If blank, that
+// means TailFS is disabled on this platform.
+func DefaultAutomountPath() string {
+ return "/Volumes/tailscale"
+}
+
+func MountShares(location string, username string) {
+ u, err := user.Lookup(username)
+ if err != nil {
+ log.Printf("warning: error looking up user %q, won't automount shares: %s", username, err)
+ return
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ log.Printf("warning: failed to parse uid %q, won't automount shares: %s", u.Uid, err)
+ }
+ gid, err := strconv.Atoi(u.Gid)
+ if err != nil {
+ log.Printf("warning: failed to parse gid %q, won't automount shares: %s", u.Gid, err)
+ }
+
+ location = filepath.Clean(location)
+ err = os.MkdirAll(location, 0700)
+ if err != nil {
+ log.Printf("warning: can't make automount location %q: %s", location, err)
+ return
+ }
+
+ err = os.Chown(location, uid, gid)
+ if err != nil {
+ log.Printf("warning: failed to chown automount location, won't automount shares: %s", err)
+ }
+
+ out, err := exec.Command("sudo", "-u", username, "mount", "-t", "webdav", "http://100.100.100.100:8080", location).CombinedOutput()
+ if err != nil {
+ log.Printf("warning: can't automount shares at %q: %s", location, out)
+ }
+}
+
+func UnmountShares(location string) {
+ location = filepath.Clean(location)
+ out, err := exec.Command("diskutil", "umount", location).CombinedOutput()
+ if err != nil {
+ log.Printf("warning: can't unmount shares from %q: %s", location, out)
+ }
+}
diff --git a/tailfs/automount_nondarwin.go b/tailfs/automount_nondarwin.go
new file mode 100644
index 000000000..dcd6df397
--- /dev/null
+++ b/tailfs/automount_nondarwin.go
@@ -0,0 +1,20 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !darwin
+
+package tailfs
+
+// DefaultAutomountPath returns the default automount path. If blank, that
+// means TailFS is disabled on this platform.
+func DefaultAutomountPath() string {
+ return ""
+}
+
+func MountShares(location string, username string) {
+ // Do nothing.
+}
+
+func UnmountShares(location string) {
+ // Do nothing.
+}