feature/taildrop: move rest of Taildrop out of LocalBackend

Updates #12614

Change-Id: If451dec1d796f6a4216fe485975c87f0c62a53e5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Co-authored-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-05-02 17:49:23 -07:00
committed by Brad Fitzpatrick
parent cf6a593196
commit 068d5ab655
21 changed files with 691 additions and 555 deletions

View File

@@ -336,11 +336,8 @@ func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Rem
}
// Check that the peer is allowed to share with us.
addresses := peer.Addresses()
for _, p := range addresses.All() {
if cn.PeerHasCap(p.Addr(), tailcfg.PeerCapabilityTaildriveSharer) {
return true
}
if cn.PeerHasCap(peer, tailcfg.PeerCapabilityTaildriveSharer) {
return true
}
return false

View File

@@ -87,6 +87,8 @@ type ExtensionHost struct {
shuttingDown atomic.Bool
extByType sync.Map // reflect.Type -> ipnext.Extension
// mu protects the following fields.
// It must not be held when calling [LocalBackend] methods
// or when invoking callbacks registered by extensions.
@@ -117,6 +119,9 @@ type Backend interface {
SwitchToBestProfile(reason string)
SendNotify(ipn.Notify)
NodeBackend() ipnext.NodeBackend
ipnext.SafeBackend
}
@@ -183,6 +188,13 @@ func newExtensionHost(logf logger.Logf, b Backend, overrideExts ...*ipnext.Defin
return host, nil
}
func (h *ExtensionHost) NodeBackend() ipnext.NodeBackend {
if h == nil {
return nil
}
return h.b.NodeBackend()
}
// Init initializes the host and the extensions it manages.
func (h *ExtensionHost) Init() {
if h != nil {
@@ -229,6 +241,7 @@ func (h *ExtensionHost) init() {
h.mu.Lock()
h.activeExtensions = append(h.activeExtensions, ext)
h.extensionsByName[ext.Name()] = ext
h.extByType.Store(reflect.TypeOf(ext), ext)
h.mu.Unlock()
}
@@ -276,6 +289,29 @@ func (h *ExtensionHost) FindExtensionByName(name string) any {
// extensionIfaceType is the runtime type of the [ipnext.Extension] interface.
var extensionIfaceType = reflect.TypeFor[ipnext.Extension]()
// GetExt returns the extension of type T registered with lb.
// If lb is nil or the extension is not found, it returns zero, false.
func GetExt[T ipnext.Extension](lb *LocalBackend) (_ T, ok bool) {
var zero T
if lb == nil {
return zero, false
}
if ext, ok := lb.extHost.extensionOfType(reflect.TypeFor[T]()); ok {
return ext.(T), true
}
return zero, false
}
func (h *ExtensionHost) extensionOfType(t reflect.Type) (_ ipnext.Extension, ok bool) {
if h == nil {
return nil, false
}
if v, ok := h.extByType.Load(t); ok {
return v.(ipnext.Extension), true
}
return nil, false
}
// FindMatchingExtension implements [ipnext.ExtensionServices]
// and is also used by the [LocalBackend].
func (h *ExtensionHost) FindMatchingExtension(target any) bool {

View File

@@ -1335,8 +1335,9 @@ func (b *testBackend) Clock() tstime.Clock { return tstime.StdClock{} }
func (b *testBackend) Sys() *tsd.System {
return b.lazySys.Get(tsd.NewSystem)
}
func (b *testBackend) SendNotify(ipn.Notify) { panic("not implemented") }
func (b *testBackend) TailscaleVarRoot() string { panic("not implemented") }
func (b *testBackend) SendNotify(ipn.Notify) { panic("not implemented") }
func (b *testBackend) NodeBackend() ipnext.NodeBackend { panic("not implemented") }
func (b *testBackend) TailscaleVarRoot() string { panic("not implemented") }
func (b *testBackend) SwitchToBestProfile(reason string) {
b.mu.Lock()

View File

@@ -26,7 +26,6 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"slices"
@@ -58,6 +57,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
"tailscale.com/log/sockstatlog"
@@ -277,37 +277,23 @@ type LocalBackend struct {
capFileSharing bool // whether netMap contains the file sharing capability
capTailnetLock bool // whether netMap contains the tailnet lock capability
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo // TODO(nickkhyl): move to nodeContext
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil; TODO(nickkhyl): move to nodeContext
activeLogin string // last logged LoginName from netMap; TODO(nickkhyl): move to nodeContext (or remove? it's in [ipn.LoginProfile]).
engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint
blocked bool
keyExpired bool // TODO(nickkhyl): move to nodeContext
authURL string // non-empty if not Running; TODO(nickkhyl): move to nodeContext
authURLTime time.Time // when the authURL was received from the control server; TODO(nickkhyl): move to nodeContext
authActor ipnauth.Actor // an actor who called [LocalBackend.StartLoginInteractive] last, or nil; TODO(nickkhyl): move to nodeContext
egg bool
prevIfState *netmon.State
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
notifyWatchers map[string]*watchSession // by session ID
lastStatusTime time.Time // status.AsOf value of the last processed status update
// directFileRoot, if non-empty, means to write received files
// directly to this directory, without staging them in an
// intermediate buffered directory for "pick-up" later. If
// empty, the files are received in a daemon-owned location
// and the localapi is used to enumerate, download, and delete
// them. This is used on macOS where the GUI lifetime is the
// same as the Network Extension lifetime and we can thus avoid
// double-copying files by writing them to the right location
// immediately.
// It's also used on several NAS platforms (Synology, TrueNAS, etc)
// but in that case DoFinalRename is also set true, which moves the
// *.partial file to its final name on completion.
directFileRoot string
hostinfo *tailcfg.Hostinfo // TODO(nickkhyl): move to nodeContext
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil; TODO(nickkhyl): move to nodeContext
activeLogin string // last logged LoginName from netMap; TODO(nickkhyl): move to nodeContext (or remove? it's in [ipn.LoginProfile]).
engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint
blocked bool
keyExpired bool // TODO(nickkhyl): move to nodeContext
authURL string // non-empty if not Running; TODO(nickkhyl): move to nodeContext
authURLTime time.Time // when the authURL was received from the control server; TODO(nickkhyl): move to nodeContext
authActor ipnauth.Actor // an actor who called [LocalBackend.StartLoginInteractive] last, or nil; TODO(nickkhyl): move to nodeContext
egg bool
prevIfState *netmon.State
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
notifyWatchers map[string]*watchSession // by session ID
lastStatusTime time.Time // status.AsOf value of the last processed status update
componentLogUntil map[string]componentLogState
// c2nUpdateStatus is the status of c2n-triggered client update.
c2nUpdateStatus updateStatus
@@ -371,9 +357,6 @@ type LocalBackend struct {
// http://go/corp/25168
lastKnownHardwareAddrs syncs.AtomicValue[[]string]
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
outgoingFiles map[string]*ipn.OutgoingFile
// lastSuggestedExitNode stores the last suggested exit node suggestion to
// avoid unnecessary churn between multiple equally-good options.
lastSuggestedExitNode tailcfg.StableNodeID
@@ -594,6 +577,11 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
func (b *LocalBackend) Clock() tstime.Clock { return b.clock }
func (b *LocalBackend) Sys() *tsd.System { return b.sys }
// NodeBackend returns the current node's NodeBackend interface.
func (b *LocalBackend) NodeBackend() ipnext.NodeBackend {
return b.currentNode()
}
func (b *LocalBackend) currentNode() *nodeBackend {
if v := b.currentNodeAtomic.Load(); v != nil || !testenv.InTest() {
return v
@@ -772,17 +760,6 @@ func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
}
// SetDirectFileRoot sets the directory to download files to directly,
// without buffering them through an intermediate daemon-owned
// tailcfg.UserID-specific directory.
//
// This must be called before the LocalBackend starts being used.
func (b *LocalBackend) SetDirectFileRoot(dir string) {
b.mu.Lock()
defer b.mu.Unlock()
b.directFileRoot = dir
}
// ReloadConfig reloads the backend's config from disk.
//
// It returns (false, nil) if not running in declarative mode, (true, nil) on
@@ -844,6 +821,16 @@ func (b *LocalBackend) setStaticEndpointsFromConfigLocked(conf *conffile.Config)
}
}
func (b *LocalBackend) setStateLocked(state ipn.State) {
if b.state == state {
return
}
b.state = state
for _, f := range b.extHost.Hooks().BackendStateChange {
f(state)
}
}
// setConfigLockedOnEntry uses the provided config to update the backend's prefs
// and other state.
func (b *LocalBackend) setConfigLockedOnEntry(conf *conffile.Config, unlock unlockOnce) error {
@@ -1309,8 +1296,8 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
Location: p.Hostinfo().Location().AsStruct(),
Capabilities: p.Capabilities().AsSlice(),
}
if f := hookSetPeerStatusTaildropTargetLocked; f != nil {
f(b, ps, p)
for _, f := range b.extHost.Hooks().SetPeerStatus {
f(ps, p, cn)
}
if cm := p.CapMap(); cm.Len() > 0 {
ps.CapMap = make(tailcfg.NodeCapMap, cm.Len())
@@ -2357,7 +2344,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
hostinfo.Services = b.hostinfo.Services // keep any previous services
}
b.hostinfo = hostinfo
b.state = ipn.NoState
b.setStateLocked(ipn.NoState)
cn := b.currentNode()
if opts.UpdatePrefs != nil {
@@ -3316,17 +3303,6 @@ func (b *LocalBackend) sendTo(n ipn.Notify, recipient notificationTarget) {
b.sendToLocked(n, recipient)
}
var (
// hookSetNotifyFilesWaitingLocked, if non-nil, is called in sendToLocked to
// populate ipn.Notify.FilesWaiting when taildrop is linked in to the binary
// and enabled on a LocalBackend.
hookSetNotifyFilesWaitingLocked func(*LocalBackend, *ipn.Notify)
// hookSetPeerStatusTaildropTargetLocked, if non-nil, is called to populate PeerStatus
// if taildrop is linked in to the binary and enabled on the LocalBackend.
hookSetPeerStatusTaildropTargetLocked func(*LocalBackend, *ipnstate.PeerStatus, tailcfg.NodeView)
)
// sendToLocked is like [LocalBackend.sendTo], but assumes b.mu is already held.
func (b *LocalBackend) sendToLocked(n ipn.Notify, recipient notificationTarget) {
if n.Prefs != nil {
@@ -3336,8 +3312,8 @@ func (b *LocalBackend) sendToLocked(n ipn.Notify, recipient notificationTarget)
n.Version = version.Long()
}
if f := hookSetNotifyFilesWaitingLocked; f != nil {
f(b, &n)
for _, f := range b.extHost.Hooks().MutateNotifyLocked {
f(&n)
}
for _, sess := range b.notifyWatchers {
@@ -5266,26 +5242,6 @@ func (b *LocalBackend) TailscaleVarRoot() string {
return ""
}
func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
if v := b.directFileRoot; v != "" {
return v
}
varRoot := b.TailscaleVarRoot()
if varRoot == "" {
b.logf("Taildrop disabled; no state directory")
return ""
}
baseDir := fmt.Sprintf("%s-uid-%d",
strings.ReplaceAll(b.activeLogin, "@", "-"),
uid)
dir := filepath.Join(varRoot, "files", baseDir)
if err := os.MkdirAll(dir, 0700); err != nil {
b.logf("Taildrop disabled; error making directory: %v", err)
return ""
}
return dir
}
// closePeerAPIListenersLocked closes any existing PeerAPI listeners
// and clears out the PeerAPI server state.
//
@@ -5353,8 +5309,7 @@ func (b *LocalBackend) initPeerAPIListener() {
}
ps := &peerAPIServer{
b: b,
taildrop: b.newTaildropManager(b.fileRootLocked(selfNode.User())),
b: b,
}
if dm, ok := b.sys.DNSManager.GetOK(); ok {
ps.resolver = dm.Resolver()
@@ -5643,7 +5598,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlockOnce) {
cn := b.currentNode()
oldState := b.state
b.state = newState
b.setStateLocked(newState)
prefs := b.pm.CurrentPrefs()
// Some temporary (2024-05-05) debugging code to help us catch
@@ -6158,6 +6113,8 @@ func (nb *nodeBackend) SetNetMap(nm *netmap.NetworkMap) {
// received nm. If nm is nil, it resets all configuration as though
// Tailscale is turned off.
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
oldSelf := b.currentNode().NetMap().SelfNodeOrZero()
b.dialer.SetNetMap(nm)
if ns, ok := b.sys.Netstack.GetOK(); ok {
ns.UpdateNetstackIPs(nm)
@@ -6205,6 +6162,13 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
b.ipVIPServiceMap = nm.GetIPVIPServiceMap()
if !oldSelf.Equal(nm.SelfNodeOrZero()) {
for _, f := range b.extHost.Hooks().OnSelfChange {
f(nm.SelfNode)
}
}
if nm == nil {
// If there is no netmap, the client is going into a "turned off"
// state so reset the metrics.
@@ -6667,12 +6631,21 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeK
return mk, nk
}
// PeerHasCap reports whether the peer with the given Tailscale IP addresses
// contains the given capability string, with any value(s).
func (nb *nodeBackend) PeerHasCap(addr netip.Addr, wantCap tailcfg.PeerCapability) bool {
// PeerHasCap reports whether the peer contains the given capability string,
// with any value(s).
func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool {
if !peer.Valid() {
return false
}
nb.mu.Lock()
defer nb.mu.Unlock()
return nb.peerHasCapLocked(addr, wantCap)
for _, ap := range peer.Addresses().All() {
if nb.peerHasCapLocked(ap.Addr(), wantCap) {
return true
}
}
return false
}
func (nb *nodeBackend) peerHasCapLocked(addr netip.Addr, wantCap tailcfg.PeerCapability) bool {

View File

@@ -60,8 +60,6 @@ type peerDNSQueryHandler interface {
type peerAPIServer struct {
b *LocalBackend
resolver peerDNSQueryHandler
taildrop *taildrop_Manager
}
func (s *peerAPIServer) listen(ip netip.Addr, ifState *netmon.State) (ln net.Listener, err error) {

View File

@@ -1,280 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_taildrop
package ipnlocal
import (
"cmp"
"context"
"errors"
"io"
"maps"
"slices"
"strings"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/taildrop"
"tailscale.com/tstime"
"tailscale.com/types/empty"
"tailscale.com/util/set"
)
func init() {
hookSetNotifyFilesWaitingLocked = (*LocalBackend).setNotifyFilesWaitingLocked
hookSetPeerStatusTaildropTargetLocked = (*LocalBackend).setPeerStatusTaildropTargetLocked
}
type taildrop_Manager = taildrop.Manager
func (b *LocalBackend) newTaildropManager(fileRoot string) *taildrop.Manager {
// TODO(bradfitz): move all this to an ipnext so ipnlocal doesn't need to depend
// on taildrop at all.
if fileRoot == "" {
b.logf("no Taildrop directory configured")
}
return taildrop.ManagerOptions{
Logf: b.logf,
Clock: tstime.DefaultClock{Clock: b.clock},
State: b.store,
Dir: fileRoot,
DirectFileMode: b.directFileRoot != "",
SendFileNotify: b.sendFileNotify,
}.New()
}
func (b *LocalBackend) sendFileNotify() {
var n ipn.Notify
b.mu.Lock()
for _, wakeWaiter := range b.fileWaiters {
wakeWaiter()
}
apiSrv := b.peerAPIServer
if apiSrv == nil {
b.mu.Unlock()
return
}
n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
b.mu.Unlock()
b.send(n)
}
// TaildropManager returns the taildrop manager for this backend.
//
// TODO(bradfitz): as of 2025-04-15, this is a temporary method during
// refactoring; the plan is for all taildrop code to leave the ipnlocal package
// and move to an extension. Baby steps.
func (b *LocalBackend) TaildropManager() (*taildrop.Manager, error) {
b.mu.Lock()
ps := b.peerAPIServer
b.mu.Unlock()
if ps == nil {
return nil, errors.New("no peer API server initialized")
}
if ps.taildrop == nil {
return nil, errors.New("no taildrop manager initialized")
}
return ps.taildrop, nil
}
func (b *LocalBackend) taildropOrNil() *taildrop.Manager {
b.mu.Lock()
ps := b.peerAPIServer
b.mu.Unlock()
if ps == nil {
return nil
}
return ps.taildrop
}
func (b *LocalBackend) setNotifyFilesWaitingLocked(n *ipn.Notify) {
if ps := b.peerAPIServer; ps != nil {
if ps.taildrop.HasFilesWaiting() {
n.FilesWaiting = &empty.Message{}
}
}
}
func (b *LocalBackend) setPeerStatusTaildropTargetLocked(ps *ipnstate.PeerStatus, p tailcfg.NodeView) {
ps.TaildropTarget = b.taildropTargetStatus(p)
}
func (b *LocalBackend) removeFileWaiter(handle set.Handle) {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.fileWaiters, handle)
}
func (b *LocalBackend) addFileWaiter(wakeWaiter context.CancelFunc) set.Handle {
b.mu.Lock()
defer b.mu.Unlock()
return b.fileWaiters.Add(wakeWaiter)
}
func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
return b.taildropOrNil().WaitingFiles()
}
// AwaitWaitingFiles is like WaitingFiles but blocks while ctx is not done,
// waiting for any files to be available.
//
// On return, exactly one of the results will be non-empty or non-nil,
// respectively.
func (b *LocalBackend) AwaitWaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 {
return ff, err
}
for {
gotFile, gotFileCancel := context.WithCancel(context.Background())
defer gotFileCancel()
handle := b.addFileWaiter(gotFileCancel)
defer b.removeFileWaiter(handle)
// Now that we've registered ourselves, check again, in case
// of race. Otherwise there's a small window where we could
// miss a file arrival and wait forever.
if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 {
return ff, err
}
select {
case <-gotFile.Done():
if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 {
return ff, err
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
func (b *LocalBackend) DeleteFile(name string) error {
return b.taildropOrNil().DeleteFile(name)
}
func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err error) {
return b.taildropOrNil().OpenFile(name)
}
// HasCapFileSharing reports whether the current node has the file
// sharing capability enabled.
func (b *LocalBackend) HasCapFileSharing() bool {
// TODO(bradfitz): remove this method and all Taildrop/Taildrive
// references from LocalBackend as part of tailscale/tailscale#12614.
b.mu.Lock()
defer b.mu.Unlock()
return b.capFileSharing
}
// FileTargets lists nodes that the current node can send files to.
func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
var ret []*apitype.FileTarget
b.mu.Lock() // for b.{state,capFileSharing}
defer b.mu.Unlock()
cn := b.currentNode()
nm := cn.NetMap()
self := cn.SelfUserID()
if b.state != ipn.Running || nm == nil {
return nil, errors.New("not connected to the tailnet")
}
if !b.capFileSharing {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
peers := cn.AppendMatchingPeers(nil, func(p tailcfg.NodeView) bool {
if !p.Valid() || p.Hostinfo().OS() == "tvOS" {
return false
}
if self == p.User() {
return true
}
if p.Addresses().Len() != 0 && cn.PeerHasCap(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
// Explicitly noted in the netmap ACL caps as a target.
return true
}
return false
})
for _, p := range peers {
peerAPI := cn.PeerAPIBase(p)
if peerAPI == "" {
continue
}
ret = append(ret, &apitype.FileTarget{
Node: p.AsStruct(),
PeerAPIURL: peerAPI,
})
}
slices.SortFunc(ret, func(a, b *apitype.FileTarget) int {
return cmp.Compare(a.Node.Name, b.Node.Name)
})
return ret, nil
}
func (b *LocalBackend) taildropTargetStatus(p tailcfg.NodeView) ipnstate.TaildropTargetStatus {
if b.state != ipn.Running {
return ipnstate.TaildropTargetIpnStateNotRunning
}
cn := b.currentNode()
nm := cn.NetMap()
if nm == nil {
return ipnstate.TaildropTargetNoNetmapAvailable
}
if !b.capFileSharing {
return ipnstate.TaildropTargetMissingCap
}
if !p.Online().Get() {
return ipnstate.TaildropTargetOffline
}
if !p.Valid() {
return ipnstate.TaildropTargetNoPeerInfo
}
if nm.User() != p.User() {
// Different user must have the explicit file sharing target capability
if p.Addresses().Len() == 0 || !cn.PeerHasCap(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
// Explicitly noted in the netmap ACL caps as a target.
return ipnstate.TaildropTargetOwnedByOtherUser
}
}
if p.Hostinfo().OS() == "tvOS" {
return ipnstate.TaildropTargetUnsupportedOS
}
if !cn.PeerHasPeerAPI(p) {
return ipnstate.TaildropTargetNoPeerAPI
}
return ipnstate.TaildropTargetAvailable
}
// UpdateOutgoingFiles updates b.outgoingFiles to reflect the given updates and
// sends an ipn.Notify with the full list of outgoingFiles.
func (b *LocalBackend) UpdateOutgoingFiles(updates map[string]*ipn.OutgoingFile) {
b.mu.Lock()
if b.outgoingFiles == nil {
b.outgoingFiles = make(map[string]*ipn.OutgoingFile, len(updates))
}
maps.Copy(b.outgoingFiles, updates)
outgoingFiles := make([]*ipn.OutgoingFile, 0, len(b.outgoingFiles))
for _, file := range b.outgoingFiles {
outgoingFiles = append(outgoingFiles, file)
}
b.mu.Unlock()
slices.SortFunc(outgoingFiles, func(a, b *ipn.OutgoingFile) int {
t := a.Started.Compare(b.Started)
if t != 0 {
return t
}
return strings.Compare(a.Name, b.Name)
})
b.send(ipn.Notify{OutgoingFiles: outgoingFiles})
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ts_omit_taildrop
package ipnlocal
type taildrop_Manager = struct{}
func (b *LocalBackend) newTaildropManager(fileRoot string) *taildrop_Manager {
return nil
}

View File

@@ -1,75 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_taildrop
package ipnlocal
import (
"fmt"
"testing"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/tstest/deptest"
"tailscale.com/types/netmap"
)
func TestFileTargets(t *testing.T) {
b := new(LocalBackend)
_, err := b.FileTargets()
if got, want := fmt.Sprint(err), "not connected to the tailnet"; got != want {
t.Errorf("before connect: got %q; want %q", got, want)
}
b.currentNode().SetNetMap(new(netmap.NetworkMap))
_, err = b.FileTargets()
if got, want := fmt.Sprint(err), "not connected to the tailnet"; got != want {
t.Errorf("non-running netmap: got %q; want %q", got, want)
}
b.state = ipn.Running
_, err = b.FileTargets()
if got, want := fmt.Sprint(err), "file sharing not enabled by Tailscale admin"; got != want {
t.Errorf("without cap: got %q; want %q", got, want)
}
b.capFileSharing = true
got, err := b.FileTargets()
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("unexpected %d peers", len(got))
}
nm := &netmap.NetworkMap{
Peers: []tailcfg.NodeView{
(&tailcfg.Node{
ID: 1234,
Hostinfo: (&tailcfg.Hostinfo{OS: "tvOS"}).View(),
}).View(),
},
}
b.currentNode().SetNetMap(nm)
got, err = b.FileTargets()
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("unexpected %d peers", len(got))
}
// (other cases handled by TestPeerAPIBase above)
}
func TestOmitTaildropDeps(t *testing.T) {
deptest.DepChecker{
Tags: "ts_omit_taildrop",
GOOS: "linux",
GOARCH: "amd64",
BadDeps: map[string]string{
"tailscale.com/taildrop": "should be omitted",
"tailscale.com/feature/taildrop": "should be omitted",
},
}.Check(t)
}