فهرست منبع

ipn/ipnlocal: add file sharing to windows shell

Updates: tailscale/winmin#33

Signed-off-by: Aleksandar Pesic <[email protected]>
Aleksandar Pesic 4 سال پیش
والد
کامیت
7c985e4944

+ 1 - 0
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+

+ 4 - 0
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)

+ 7 - 1
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)
 	}

+ 24 - 15
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

+ 4 - 17
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,
 	}

+ 13 - 0
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) {}

+ 107 - 0
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)
+	}
+}