mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-25 10:14:36 +00:00

This adds a feature/taildrop package, a ts_omit_taildrop build tag, and starts moving code to feature/taildrop. In some cases, code remains where it was but is now behind a build tag. Future changes will move code to an extension and out of LocalBackend, etc. Updates #12614 Change-Id: Idf96c61144d1a5f707039ceb2ff59c99f5c1642f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
290 lines
7.5 KiB
Go
290 lines
7.5 KiB
Go
// 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()
|
|
defer b.mu.Unlock()
|
|
nm := b.netMap
|
|
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")
|
|
}
|
|
for _, p := range b.peers {
|
|
if !b.peerIsTaildropTargetLocked(p) {
|
|
continue
|
|
}
|
|
if p.Hostinfo().OS() == "tvOS" {
|
|
continue
|
|
}
|
|
peerAPI := peerAPIBase(b.netMap, 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
|
|
}
|
|
if b.netMap == nil {
|
|
return ipnstate.TaildropTargetNoNetmapAvailable
|
|
}
|
|
if !b.capFileSharing {
|
|
return ipnstate.TaildropTargetMissingCap
|
|
}
|
|
|
|
if !p.Online().Get() {
|
|
return ipnstate.TaildropTargetOffline
|
|
}
|
|
|
|
if !p.Valid() {
|
|
return ipnstate.TaildropTargetNoPeerInfo
|
|
}
|
|
if b.netMap.User() != p.User() {
|
|
// Different user must have the explicit file sharing target capability
|
|
if p.Addresses().Len() == 0 ||
|
|
!b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
|
|
return ipnstate.TaildropTargetOwnedByOtherUser
|
|
}
|
|
}
|
|
|
|
if p.Hostinfo().OS() == "tvOS" {
|
|
return ipnstate.TaildropTargetUnsupportedOS
|
|
}
|
|
if peerAPIBase(b.netMap, p) == "" {
|
|
return ipnstate.TaildropTargetNoPeerAPI
|
|
}
|
|
return ipnstate.TaildropTargetAvailable
|
|
}
|
|
|
|
// peerIsTaildropTargetLocked reports whether p is a valid Taildrop file
|
|
// recipient from this node according to its ownership and the capabilities in
|
|
// the netmap.
|
|
//
|
|
// b.mu must be locked.
|
|
func (b *LocalBackend) peerIsTaildropTargetLocked(p tailcfg.NodeView) bool {
|
|
if b.netMap == nil || !p.Valid() {
|
|
return false
|
|
}
|
|
if b.netMap.User() == p.User() {
|
|
return true
|
|
}
|
|
if p.Addresses().Len() > 0 &&
|
|
b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
|
|
// Explicitly noted in the netmap ACL caps as a target.
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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})
|
|
}
|