ipn/ipnlocal: add file sharing to windows shell

Updates: tailscale/winmin#33

Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
This commit is contained in:
Aleksandar Pesic 2021-04-22 09:25:00 +02:00 committed by Brad Fitzpatrick
parent e41075dd4a
commit 7c985e4944
7 changed files with 160 additions and 33 deletions

View File

@ -134,6 +134,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+ tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
LW tailscale.com/util/endian from tailscale.com/net/netns+ LW tailscale.com/util/endian from tailscale.com/net/netns+
L tailscale.com/util/lineread from tailscale.com/control/controlclient+ 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/pidowner from tailscale.com/ipn/ipnserver
tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/systemd from tailscale.com/control/controlclient+ tailscale.com/util/systemd from tailscale.com/control/controlclient+

View File

@ -16,6 +16,7 @@
"golang.org/x/sys/windows/svc/mgr" "golang.org/x/sys/windows/svc/mgr"
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/osshare"
) )
func init() { func init() {
@ -79,6 +80,9 @@ func installSystemDaemonWindows(args []string) (err error) {
} }
func uninstallSystemDaemonWindows(args []string) (ret 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() m, err := mgr.Connect()
if err != nil { if err != nil {
return fmt.Errorf("failed to connect to Windows service manager: %v", err) return fmt.Errorf("failed to connect to Windows service manager: %v", err)

View File

@ -40,6 +40,7 @@
"tailscale.com/types/flagtype" "tailscale.com/types/flagtype"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/util/osshare"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/version/distro" "tailscale.com/version/distro"
"tailscale.com/wgengine" "tailscale.com/wgengine"
@ -160,7 +161,12 @@ func main() {
log.Fatalf("--socket is required") 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 // No need to log; the func already did
os.Exit(1) os.Exit(1)
} }

View File

@ -46,6 +46,7 @@
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/wgkey" "tailscale.com/types/wgkey"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/osshare"
"tailscale.com/util/systemd" "tailscale.com/util/systemd"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/wgengine" "tailscale.com/wgengine"
@ -105,6 +106,7 @@ type LocalBackend struct {
inServerMode bool inServerMode bool
machinePrivKey wgkey.Private machinePrivKey wgkey.Private
state ipn.State state ipn.State
capFileSharing bool // whether netMap contains the file sharing capability
// hostinfo is mutated in-place while mu is held. // hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set. // 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") panic("ipn.NewLocalBackend: wgengine must not be nil")
} }
osshare.SetFileSharingEnabled(false, logf)
// Default filter blocks everything and logs nothing, until Start() is called. // Default filter blocks everything and logs nothing, until Start() is called.
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{})) e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
@ -2256,6 +2260,17 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
cc.SetNetInfo(ni) 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) { func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
var login string var login string
if nm != nil { if nm != nil {
@ -2270,6 +2285,13 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.activeLogin = login 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 { if nm == nil {
b.nodeByAddr = nil b.nodeByAddr = nil
return return
@ -2378,20 +2400,7 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
func (b *LocalBackend) hasCapFileSharing() bool { func (b *LocalBackend) hasCapFileSharing() bool {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
return b.hasCapFileSharingLocked() return b.capFileSharing
}
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
} }
// FileTargets lists nodes that the current node can send files to. // 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() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
if !b.hasCapFileSharingLocked() { if !b.capFileSharing {
return nil, errors.New("file sharing not enabled by Tailscale admin") return nil, errors.New("file sharing not enabled by Tailscale admin")
} }
nm := b.netMap nm := b.netMap

View File

@ -19,7 +19,6 @@
"testing" "testing"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/netmap"
) )
type peerAPITestEnv struct { type peerAPITestEnv struct {
@ -391,18 +390,10 @@ func TestHandlePeerAPI(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var caps []string
if tt.capSharing {
caps = append(caps, tailcfg.CapabilityFileSharing)
}
var e peerAPITestEnv var e peerAPITestEnv
lb := &LocalBackend{ lb := &LocalBackend{
netMap: &netmap.NetworkMap{
SelfNode: &tailcfg.Node{
Capabilities: caps,
},
},
logf: e.logf, logf: e.logf,
capFileSharing: tt.capSharing,
} }
e.ph = &peerAPIHandler{ e.ph = &peerAPIHandler{
isSelf: tt.isSelf, isSelf: tt.isSelf,
@ -447,11 +438,7 @@ func TestFileDeleteRace(t *testing.T) {
ps := &peerAPIServer{ ps := &peerAPIServer{
b: &LocalBackend{ b: &LocalBackend{
logf: t.Logf, logf: t.Logf,
netMap: &netmap.NetworkMap{ capFileSharing: true,
SelfNode: &tailcfg.Node{
Capabilities: []string{tailcfg.CapabilityFileSharing},
},
},
}, },
rootDir: dir, rootDir: dir,
} }

View File

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

View File

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