diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 4ac6ab97a..e51b75dec 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -134,6 +134,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+ LW tailscale.com/util/endian from tailscale.com/net/netns+ L tailscale.com/util/lineread from tailscale.com/control/controlclient+ + tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+ tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/systemd from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/install_windows.go b/cmd/tailscaled/install_windows.go index 2f890967d..3350779d1 100644 --- a/cmd/tailscaled/install_windows.go +++ b/cmd/tailscaled/install_windows.go @@ -16,6 +16,7 @@ import ( "golang.org/x/sys/windows/svc/mgr" "tailscale.com/logtail/backoff" "tailscale.com/types/logger" + "tailscale.com/util/osshare" ) func init() { @@ -79,6 +80,9 @@ func installSystemDaemonWindows(args []string) (err error) { } func uninstallSystemDaemonWindows(args []string) (ret error) { + // Remove file sharing from Windows shell (noop in non-windows) + osshare.SetFileSharingEnabled(false, logger.Discard) + m, err := mgr.Connect() if err != nil { return fmt.Errorf("failed to connect to Windows service manager: %v", err) diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 847f137c5..12311c457 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -40,6 +40,7 @@ import ( "tailscale.com/types/flagtype" "tailscale.com/types/logger" "tailscale.com/types/netmap" + "tailscale.com/util/osshare" "tailscale.com/version" "tailscale.com/version/distro" "tailscale.com/wgengine" @@ -160,7 +161,12 @@ func main() { log.Fatalf("--socket is required") } - if err := run(); err != nil { + err := run() + + // Remove file sharing from Windows shell (noop in non-windows) + osshare.SetFileSharingEnabled(false, logger.Discard) + + if err != nil { // No need to log; the func already did os.Exit(1) } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b69ee54d7..76794e86c 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -46,6 +46,7 @@ import ( "tailscale.com/types/persist" "tailscale.com/types/wgkey" "tailscale.com/util/dnsname" + "tailscale.com/util/osshare" "tailscale.com/util/systemd" "tailscale.com/version" "tailscale.com/wgengine" @@ -105,6 +106,7 @@ type LocalBackend struct { inServerMode bool machinePrivKey wgkey.Private state ipn.State + capFileSharing bool // whether netMap contains the file sharing capability // hostinfo is mutated in-place while mu is held. hostinfo *tailcfg.Hostinfo // netMap is not mutated in-place once set. @@ -145,6 +147,8 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge panic("ipn.NewLocalBackend: wgengine must not be nil") } + osshare.SetFileSharingEnabled(false, logf) + // Default filter blocks everything and logs nothing, until Start() is called. e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{})) @@ -2256,6 +2260,17 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) { cc.SetNetInfo(ni) } +func hasCapability(nm *netmap.NetworkMap, cap string) bool { + if nm != nil && nm.SelfNode != nil { + for _, c := range nm.SelfNode.Capabilities { + if c == cap { + return true + } + } + } + return false +} + func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { var login string if nm != nil { @@ -2270,6 +2285,13 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { b.activeLogin = login } + // Determine if file sharing is enabled + fs := hasCapability(nm, tailcfg.CapabilityFileSharing) + if fs != b.capFileSharing { + osshare.SetFileSharingEnabled(fs, b.logf) + } + b.capFileSharing = fs + if nm == nil { b.nodeByAddr = nil return @@ -2378,20 +2400,7 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err func (b *LocalBackend) hasCapFileSharing() bool { b.mu.Lock() defer b.mu.Unlock() - return b.hasCapFileSharingLocked() -} - -func (b *LocalBackend) hasCapFileSharingLocked() bool { - nm := b.netMap - if nm == nil || nm.SelfNode == nil { - return false - } - for _, c := range nm.SelfNode.Capabilities { - if c == tailcfg.CapabilityFileSharing { - return true - } - } - return false + return b.capFileSharing } // FileTargets lists nodes that the current node can send files to. @@ -2400,7 +2409,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) { b.mu.Lock() defer b.mu.Unlock() - if !b.hasCapFileSharingLocked() { + if !b.capFileSharing { return nil, errors.New("file sharing not enabled by Tailscale admin") } nm := b.netMap diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go index 2af09e4d0..207b941cc 100644 --- a/ipn/ipnlocal/peerapi_test.go +++ b/ipn/ipnlocal/peerapi_test.go @@ -19,7 +19,6 @@ import ( "testing" "tailscale.com/tailcfg" - "tailscale.com/types/netmap" ) type peerAPITestEnv struct { @@ -391,18 +390,10 @@ func TestHandlePeerAPI(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var caps []string - if tt.capSharing { - caps = append(caps, tailcfg.CapabilityFileSharing) - } var e peerAPITestEnv lb := &LocalBackend{ - netMap: &netmap.NetworkMap{ - SelfNode: &tailcfg.Node{ - Capabilities: caps, - }, - }, - logf: e.logf, + logf: e.logf, + capFileSharing: tt.capSharing, } e.ph = &peerAPIHandler{ isSelf: tt.isSelf, @@ -446,12 +437,8 @@ func TestFileDeleteRace(t *testing.T) { dir := t.TempDir() ps := &peerAPIServer{ b: &LocalBackend{ - logf: t.Logf, - netMap: &netmap.NetworkMap{ - SelfNode: &tailcfg.Node{ - Capabilities: []string{tailcfg.CapabilityFileSharing}, - }, - }, + logf: t.Logf, + capFileSharing: true, }, rootDir: dir, } diff --git a/util/osshare/filesharingstatus_noop.go b/util/osshare/filesharingstatus_noop.go new file mode 100644 index 000000000..d2fe1a377 --- /dev/null +++ b/util/osshare/filesharingstatus_noop.go @@ -0,0 +1,13 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !windows + +package osshare + +import ( + "tailscale.com/types/logger" +) + +func SetFileSharingEnabled(enabled bool, logf logger.Logf) {} diff --git a/util/osshare/filesharingstatus_windows.go b/util/osshare/filesharingstatus_windows.go new file mode 100644 index 000000000..c3282994f --- /dev/null +++ b/util/osshare/filesharingstatus_windows.go @@ -0,0 +1,107 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package osshare + +import ( + "fmt" + "os" + "path/filepath" + "sync" + + "golang.org/x/sys/windows/registry" + "tailscale.com/types/logger" +) + +const ( + sendFileShellKey = `*\shell\tailscale` +) + +var ipnExePath struct { + sync.Mutex + cache string // absolute path of tailscale-ipn.exe, populated lazily on first use +} + +func getIpnExePath(logf logger.Logf) string { + ipnExePath.Lock() + defer ipnExePath.Unlock() + + if ipnExePath.cache != "" { + return ipnExePath.cache + } + + // Find the absolute path of tailscale-ipn.exe assuming that it's in the same + // directory as this executable (tailscaled.exe). + p, err := os.Executable() + if err != nil { + logf("os.Executable error: %v", err) + return "" + } + if p, err = filepath.EvalSymlinks(p); err != nil { + logf("filepath.EvalSymlinks error: %v", err) + return "" + } + p = filepath.Join(filepath.Dir(p), "tailscale-ipn.exe") + if p, err = filepath.Abs(p); err != nil { + logf("filepath.Abs error: %v", err) + return "" + } + ipnExePath.cache = p + + return p +} + +// SetFileSharingEnabled adds/removes "Send with Tailscale" from the Windows shell menu. +func SetFileSharingEnabled(enabled bool, logf logger.Logf) { + logf = logger.WithPrefix(logf, fmt.Sprintf("SetFileSharingEnabled(%v) error: ", enabled)) + if enabled { + enableFileSharing(logf) + } else { + disableFileSharing(logf) + } +} + +func enableFileSharing(logf logger.Logf) { + path := getIpnExePath(logf) + if path == "" { + return + } + + k, _, err := registry.CreateKey(registry.CLASSES_ROOT, sendFileShellKey, registry.WRITE) + if err != nil { + logf("failed to create HKEY_CLASSES_ROOT\\%s reg key: %v", sendFileShellKey, err) + return + } + defer k.Close() + if err := k.SetStringValue("", "Send with Tailscale..."); err != nil { + logf("k.SetStringValue error: %v", err) + return + } + if err := k.SetStringValue("Icon", path+",0"); err != nil { + logf("k.SetStringValue error: %v", err) + return + } + c, _, err := registry.CreateKey(k, "command", registry.WRITE) + if err != nil { + logf("failed to create HKEY_CLASSES_ROOT\\%s\\command reg key: %v", sendFileShellKey, err) + return + } + defer c.Close() + if err := c.SetStringValue("", "\""+path+"\" /push \"%1\""); err != nil { + logf("c.SetStringValue error: %v", err) + } +} + +func disableFileSharing(logf logger.Logf) { + if err := registry.DeleteKey(registry.CLASSES_ROOT, sendFileShellKey+"\\command"); err != nil && + err != registry.ErrNotExist { + logf("registry.DeleteKey error: %v\n", err) + return + } + if err := registry.DeleteKey(registry.CLASSES_ROOT, sendFileShellKey); err != nil && err != registry.ErrNotExist { + logf("registry.DeleteKey error: %v\n", err) + } +}