mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-09 17:16:50 +00:00
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:
parent
cf6a593196
commit
068d5ab655
@ -908,7 +908,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/sessionrecording from tailscale.com/k8s-operator/sessionrecording+
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/taildrop from tailscale.com/feature/taildrop
|
||||
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tempfork/httprec from tailscale.com/control/controlclient
|
||||
@ -965,7 +965,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/ipn/localapi
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/osuser from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/progresstracking from tailscale.com/feature/taildrop
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
|
@ -359,7 +359,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/taildrop from tailscale.com/feature/taildrop
|
||||
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
|
@ -1,15 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_taildrop
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
|
||||
// Nothing.
|
||||
}
|
@ -660,7 +660,6 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
Socket: args.socketpath,
|
||||
UseSocketOnly: args.socketpath != paths.DefaultTailscaledSocket(),
|
||||
})
|
||||
configureTaildrop(logf, lb)
|
||||
if err := ns.Start(lb); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ func (e *extension) relayServerOrInit() (relayServer, error) {
|
||||
}
|
||||
|
||||
func handlePeerAPIRelayAllocateEndpoint(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
|
||||
e, ok := h.LocalBackend().FindExtensionByName(featureName).(*extension)
|
||||
e, ok := ipnlocal.GetExt[*extension](h.LocalBackend())
|
||||
if !ok {
|
||||
http.Error(w, "relay failed to initialize", http.StatusServiceUnavailable)
|
||||
return
|
||||
|
@ -4,10 +4,30 @@
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -15,38 +35,374 @@ func init() {
|
||||
}
|
||||
|
||||
func newExtension(logf logger.Logf, b ipnext.SafeBackend) (ipnext.Extension, error) {
|
||||
return &extension{
|
||||
logf: logger.WithPrefix(logf, "taildrop: "),
|
||||
}, nil
|
||||
e := &Extension{
|
||||
sb: b,
|
||||
stateStore: b.Sys().StateStore.Get(),
|
||||
logf: logger.WithPrefix(logf, "taildrop: "),
|
||||
}
|
||||
e.setPlatformDefaultDirectFileRoot()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
type extension struct {
|
||||
logf logger.Logf
|
||||
sb ipnext.SafeBackend
|
||||
mgr *taildrop.Manager
|
||||
// Extension implements Taildrop.
|
||||
type Extension struct {
|
||||
logf logger.Logf
|
||||
sb ipnext.SafeBackend
|
||||
stateStore ipn.StateStore
|
||||
host ipnext.Host // from Init
|
||||
|
||||
// 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
|
||||
|
||||
nodeBackendForTest ipnext.NodeBackend // if non-nil, pretend we're this node state for tests
|
||||
|
||||
mu sync.Mutex // Lock order: lb.mu > e.mu
|
||||
backendState ipn.State
|
||||
selfUID tailcfg.UserID
|
||||
capFileSharing bool
|
||||
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
|
||||
mgr atomic.Pointer[taildrop.Manager] // mutex held to write; safe to read without lock;
|
||||
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
|
||||
outgoingFiles map[string]*ipn.OutgoingFile
|
||||
}
|
||||
|
||||
func (e *extension) Name() string {
|
||||
func (e *Extension) Name() string {
|
||||
return "taildrop"
|
||||
}
|
||||
|
||||
func (e *extension) Init(h ipnext.Host) error {
|
||||
// TODO(bradfitz): move init of taildrop.Manager from ipnlocal/peerapi.go to
|
||||
// here
|
||||
e.mgr = nil
|
||||
func (e *Extension) Init(h ipnext.Host) error {
|
||||
e.host = h
|
||||
|
||||
osshare.SetFileSharingEnabled(false, e.logf)
|
||||
|
||||
h.Hooks().ProfileStateChange.Add(e.onChangeProfile)
|
||||
h.Hooks().OnSelfChange.Add(e.onSelfChange)
|
||||
h.Hooks().MutateNotifyLocked.Add(e.setNotifyFilesWaiting)
|
||||
h.Hooks().SetPeerStatus.Add(e.setPeerStatus)
|
||||
h.Hooks().BackendStateChange.Add(e.onBackendStateChange)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *extension) Shutdown() error {
|
||||
lb, ok := e.sb.(*ipnlocal.LocalBackend)
|
||||
if !ok {
|
||||
return nil
|
||||
func (e *Extension) onBackendStateChange(st ipn.State) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.backendState = st
|
||||
}
|
||||
|
||||
func (e *Extension) onSelfChange(self tailcfg.NodeView) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.selfUID = 0
|
||||
if self.Valid() {
|
||||
e.selfUID = self.User()
|
||||
}
|
||||
if mgr, err := lb.TaildropManager(); err == nil {
|
||||
mgr.Shutdown()
|
||||
} else {
|
||||
e.logf("taildrop: failed to shutdown taildrop manager: %v", err)
|
||||
e.capFileSharing = self.Valid() && self.CapMap().Contains(tailcfg.CapabilityFileSharing)
|
||||
osshare.SetFileSharingEnabled(e.capFileSharing, e.logf)
|
||||
}
|
||||
|
||||
func (e *Extension) setMgrLocked(mgr *taildrop.Manager) {
|
||||
if old := e.mgr.Swap(mgr); old != nil {
|
||||
old.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Extension) onChangeProfile(profile ipn.LoginProfileView, _ ipn.PrefsView, sameNode bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
uid := profile.UserProfile().ID
|
||||
activeLogin := profile.UserProfile().LoginName
|
||||
|
||||
if uid == 0 {
|
||||
e.setMgrLocked(nil)
|
||||
e.outgoingFiles = nil
|
||||
return
|
||||
}
|
||||
|
||||
if sameNode && e.manager() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we have a netmap, create a taildrop manager.
|
||||
fileRoot, isDirectFileMode := e.fileRoot(uid, activeLogin)
|
||||
if fileRoot == "" {
|
||||
e.logf("no Taildrop directory configured")
|
||||
}
|
||||
e.setMgrLocked(taildrop.ManagerOptions{
|
||||
Logf: e.logf,
|
||||
Clock: tstime.DefaultClock{Clock: e.sb.Clock()},
|
||||
State: e.stateStore,
|
||||
Dir: fileRoot,
|
||||
DirectFileMode: isDirectFileMode,
|
||||
SendFileNotify: e.sendFileNotify,
|
||||
}.New())
|
||||
}
|
||||
|
||||
// fileRoot returns where to store Taildrop files for the given user and whether
|
||||
// to write received files directly to this directory, without staging them in
|
||||
// an intermediate buffered directory for "pick-up" later.
|
||||
//
|
||||
// It is safe to call this with b.mu held but it does not require it or acquire
|
||||
// it itself.
|
||||
func (e *Extension) fileRoot(uid tailcfg.UserID, activeLogin string) (root string, isDirect bool) {
|
||||
if v := e.directFileRoot; v != "" {
|
||||
return v, true
|
||||
}
|
||||
varRoot := e.sb.TailscaleVarRoot()
|
||||
if varRoot == "" {
|
||||
e.logf("Taildrop disabled; no state directory")
|
||||
return "", false
|
||||
}
|
||||
|
||||
if activeLogin == "" {
|
||||
e.logf("taildrop: no active login; can't select a target directory")
|
||||
return "", false
|
||||
}
|
||||
|
||||
baseDir := fmt.Sprintf("%s-uid-%d",
|
||||
strings.ReplaceAll(activeLogin, "@", "-"),
|
||||
uid)
|
||||
dir := filepath.Join(varRoot, "files", baseDir)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
e.logf("Taildrop disabled; error making directory: %v", err)
|
||||
return "", false
|
||||
}
|
||||
return dir, false
|
||||
}
|
||||
|
||||
// hasCapFileSharing reports whether the current node has the file sharing
|
||||
// capability.
|
||||
func (e *Extension) hasCapFileSharing() bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.capFileSharing
|
||||
}
|
||||
|
||||
// manager returns the active taildrop.Manager, or nil.
|
||||
//
|
||||
// Methods on a nil Manager are safe to call.
|
||||
func (e *Extension) manager() *taildrop.Manager {
|
||||
return e.mgr.Load()
|
||||
}
|
||||
|
||||
func (e *Extension) Clock() tstime.Clock {
|
||||
return e.sb.Clock()
|
||||
}
|
||||
|
||||
func (e *Extension) Shutdown() error {
|
||||
e.manager().Shutdown() // no-op on nil receiver
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Extension) sendFileNotify() {
|
||||
mgr := e.manager()
|
||||
if mgr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var n ipn.Notify
|
||||
|
||||
e.mu.Lock()
|
||||
for _, wakeWaiter := range e.fileWaiters {
|
||||
wakeWaiter()
|
||||
}
|
||||
n.IncomingFiles = mgr.IncomingFiles()
|
||||
e.mu.Unlock()
|
||||
|
||||
e.host.SendNotifyAsync(n)
|
||||
}
|
||||
|
||||
func (e *Extension) setNotifyFilesWaiting(n *ipn.Notify) {
|
||||
if e.manager().HasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Extension) setPeerStatus(ps *ipnstate.PeerStatus, p tailcfg.NodeView, nb ipnext.NodeBackend) {
|
||||
ps.TaildropTarget = e.taildropTargetStatus(p, nb)
|
||||
}
|
||||
|
||||
func (e *Extension) removeFileWaiter(handle set.Handle) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
delete(e.fileWaiters, handle)
|
||||
}
|
||||
|
||||
func (e *Extension) addFileWaiter(wakeWaiter context.CancelFunc) set.Handle {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.fileWaiters.Add(wakeWaiter)
|
||||
}
|
||||
|
||||
func (e *Extension) WaitingFiles() ([]apitype.WaitingFile, error) {
|
||||
return e.manager().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 (e *Extension) AwaitWaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
|
||||
if ff, err := e.WaitingFiles(); err != nil || len(ff) > 0 {
|
||||
return ff, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
gotFile, gotFileCancel := context.WithCancel(context.Background())
|
||||
defer gotFileCancel()
|
||||
|
||||
handle := e.addFileWaiter(gotFileCancel)
|
||||
defer e.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 := e.WaitingFiles(); err != nil || len(ff) > 0 {
|
||||
return ff, err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-gotFile.Done():
|
||||
if ff, err := e.WaitingFiles(); err != nil || len(ff) > 0 {
|
||||
return ff, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Extension) DeleteFile(name string) error {
|
||||
return e.manager().DeleteFile(name)
|
||||
}
|
||||
|
||||
func (e *Extension) OpenFile(name string) (rc io.ReadCloser, size int64, err error) {
|
||||
return e.manager().OpenFile(name)
|
||||
}
|
||||
|
||||
func (e *Extension) nodeBackend() ipnext.NodeBackend {
|
||||
if e.nodeBackendForTest != nil {
|
||||
return e.nodeBackendForTest
|
||||
}
|
||||
return e.host.NodeBackend()
|
||||
}
|
||||
|
||||
// FileTargets lists nodes that the current node can send files to.
|
||||
func (e *Extension) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
var ret []*apitype.FileTarget
|
||||
|
||||
e.mu.Lock()
|
||||
st := e.backendState
|
||||
self := e.selfUID
|
||||
e.mu.Unlock()
|
||||
|
||||
if st != ipn.Running {
|
||||
return nil, errors.New("not connected to the tailnet")
|
||||
}
|
||||
if !e.hasCapFileSharing() {
|
||||
return nil, errors.New("file sharing not enabled by Tailscale admin")
|
||||
}
|
||||
nb := e.nodeBackend()
|
||||
peers := nb.AppendMatchingPeers(nil, func(p tailcfg.NodeView) bool {
|
||||
if !p.Valid() || p.Hostinfo().OS() == "tvOS" {
|
||||
return false
|
||||
}
|
||||
if self == p.User() {
|
||||
return true
|
||||
}
|
||||
if nb.PeerHasCap(p, tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
// Explicitly noted in the netmap ACL caps as a target.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
for _, p := range peers {
|
||||
peerAPI := nb.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 (e *Extension) taildropTargetStatus(p tailcfg.NodeView, nb ipnext.NodeBackend) ipnstate.TaildropTargetStatus {
|
||||
e.mu.Lock()
|
||||
st := e.backendState
|
||||
selfUID := e.selfUID
|
||||
capFileSharing := e.capFileSharing
|
||||
e.mu.Unlock()
|
||||
|
||||
if st != ipn.Running {
|
||||
return ipnstate.TaildropTargetIpnStateNotRunning
|
||||
}
|
||||
|
||||
if !capFileSharing {
|
||||
return ipnstate.TaildropTargetMissingCap
|
||||
}
|
||||
if !p.Valid() {
|
||||
return ipnstate.TaildropTargetNoPeerInfo
|
||||
}
|
||||
if !p.Online().Get() {
|
||||
return ipnstate.TaildropTargetOffline
|
||||
}
|
||||
if p.Hostinfo().OS() == "tvOS" {
|
||||
return ipnstate.TaildropTargetUnsupportedOS
|
||||
}
|
||||
if selfUID != p.User() {
|
||||
// Different user must have the explicit file sharing target capability
|
||||
if !nb.PeerHasCap(p, tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
return ipnstate.TaildropTargetOwnedByOtherUser
|
||||
}
|
||||
}
|
||||
if !nb.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 (e *Extension) updateOutgoingFiles(updates map[string]*ipn.OutgoingFile) {
|
||||
e.mu.Lock()
|
||||
if e.outgoingFiles == nil {
|
||||
e.outgoingFiles = make(map[string]*ipn.OutgoingFile, len(updates))
|
||||
}
|
||||
maps.Copy(e.outgoingFiles, updates)
|
||||
outgoingFiles := make([]*ipn.OutgoingFile, 0, len(e.outgoingFiles))
|
||||
for _, file := range e.outgoingFiles {
|
||||
outgoingFiles = append(outgoingFiles, file)
|
||||
}
|
||||
e.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)
|
||||
})
|
||||
|
||||
e.host.SendNotifyAsync(ipn.Notify{OutgoingFiles: outgoingFiles})
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
@ -80,9 +81,13 @@ func serveFilePut(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
lb := h.LocalBackend()
|
||||
ext, ok := ipnlocal.GetExt[*Extension](h.LocalBackend())
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured taildrop extension", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fts, err := lb.FileTargets()
|
||||
fts, err := ext.FileTargets()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -131,7 +136,7 @@ func serveFilePut(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
go func() {
|
||||
defer t.Stop()
|
||||
defer lb.UpdateOutgoingFiles(outgoingFiles)
|
||||
defer ext.updateOutgoingFiles(outgoingFiles)
|
||||
for {
|
||||
select {
|
||||
case u, ok := <-progressUpdates:
|
||||
@ -140,7 +145,7 @@ func serveFilePut(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
outgoingFiles[u.ID] = &u
|
||||
case <-t.C:
|
||||
lb.UpdateOutgoingFiles(outgoingFiles)
|
||||
ext.updateOutgoingFiles(outgoingFiles)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -301,7 +306,11 @@ func singleFilePut(
|
||||
fail()
|
||||
return false
|
||||
}
|
||||
switch resp, err := client.Do(req); {
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
switch {
|
||||
case err != nil:
|
||||
h.Logf("could not fetch remote hashes: %v", err)
|
||||
case resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotFound:
|
||||
@ -353,7 +362,13 @@ func serveFiles(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
lb := h.LocalBackend()
|
||||
|
||||
ext, ok := ipnlocal.GetExt[*Extension](h.LocalBackend())
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured taildrop extension", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/files/")
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured", http.StatusInternalServerError)
|
||||
@ -376,14 +391,14 @@ func serveFiles(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, deadline)
|
||||
defer cancel()
|
||||
wfs, err = lb.AwaitWaitingFiles(ctx)
|
||||
wfs, err = ext.AwaitWaitingFiles(ctx)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
wfs, err = lb.WaitingFiles()
|
||||
wfs, err = ext.WaitingFiles()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -399,14 +414,14 @@ func serveFiles(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method == "DELETE" {
|
||||
if err := lb.DeleteFile(name); err != nil {
|
||||
if err := ext.DeleteFile(name); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
rc, size, err := lb.OpenFile(name)
|
||||
rc, size, err := ext.OpenFile(name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -426,7 +441,14 @@ func serveFileTargets(h *localapi.Handler, w http.ResponseWriter, r *http.Reques
|
||||
http.Error(w, "want GET to list targets", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fts, err := h.LocalBackend().FileTargets()
|
||||
|
||||
ext, ok := ipnlocal.GetExt[*Extension](h.LocalBackend())
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured taildrop extension", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fts, err := ext.FileTargets()
|
||||
if err != nil {
|
||||
localapi.WriteErrorJSON(w, err)
|
||||
return
|
||||
|
@ -1,35 +1,38 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_taildrop
|
||||
|
||||
package main
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
|
||||
// SetDirectFileRoot sets the directory where received files are written.
|
||||
//
|
||||
// This must be called before Tailscale is started.
|
||||
func (e *Extension) SetDirectFileRoot(root string) {
|
||||
e.directFileRoot = root
|
||||
}
|
||||
|
||||
func (e *Extension) setPlatformDefaultDirectFileRoot() {
|
||||
dg := distro.Get()
|
||||
|
||||
switch dg {
|
||||
case distro.Synology, distro.TrueNAS, distro.QNAP, distro.Unraid:
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findTaildropDir(dg)
|
||||
if err != nil {
|
||||
logf("%s Taildrop support: %v", dg, err)
|
||||
e.logf("%s Taildrop support: %v", dg, err)
|
||||
} else {
|
||||
logf("%s Taildrop: using %v", dg, path)
|
||||
lb.SetDirectFileRoot(path)
|
||||
e.logf("%s Taildrop: using %v", dg, path)
|
||||
e.directFileRoot = path
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findTaildropDir(dg distro.Distro) (string, error) {
|
@ -38,26 +38,30 @@ func canPutFile(h ipnlocal.PeerAPIHandler) bool {
|
||||
}
|
||||
|
||||
func handlePeerPut(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
|
||||
lb := h.LocalBackend()
|
||||
handlePeerPutWithBackend(h, lb, w, r)
|
||||
ext, ok := ipnlocal.GetExt[*Extension](h.LocalBackend())
|
||||
if !ok {
|
||||
http.Error(w, "miswired", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
handlePeerPutWithBackend(h, ext, w, r)
|
||||
}
|
||||
|
||||
// localBackend is the subset of ipnlocal.Backend that taildrop
|
||||
// extensionForPut is the subset of taildrop extension that taildrop
|
||||
// file put needs. This is pulled out for testability.
|
||||
type localBackend interface {
|
||||
TaildropManager() (*taildrop.Manager, error)
|
||||
HasCapFileSharing() bool
|
||||
type extensionForPut interface {
|
||||
manager() *taildrop.Manager
|
||||
hasCapFileSharing() bool
|
||||
Clock() tstime.Clock
|
||||
}
|
||||
|
||||
func handlePeerPutWithBackend(h ipnlocal.PeerAPIHandler, lb localBackend, w http.ResponseWriter, r *http.Request) {
|
||||
func handlePeerPutWithBackend(h ipnlocal.PeerAPIHandler, ext extensionForPut, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "PUT" {
|
||||
metricPutCalls.Add(1)
|
||||
}
|
||||
|
||||
taildropMgr, err := lb.TaildropManager()
|
||||
if err != nil {
|
||||
h.Logf("taildropManager: %v", err)
|
||||
taildropMgr := ext.manager()
|
||||
if taildropMgr == nil {
|
||||
h.Logf("taildrop: no taildrop manager")
|
||||
http.Error(w, "failed to get taildrop manager", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@ -66,7 +70,7 @@ func handlePeerPutWithBackend(h ipnlocal.PeerAPIHandler, lb localBackend, w http
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !lb.HasCapFileSharing() {
|
||||
if !ext.hasCapFileSharing() {
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@ -123,7 +127,7 @@ func handlePeerPutWithBackend(h ipnlocal.PeerAPIHandler, lb localBackend, w http
|
||||
}
|
||||
}
|
||||
case "PUT":
|
||||
t0 := lb.Clock().Now()
|
||||
t0 := ext.Clock().Now()
|
||||
id := taildrop.ClientID(h.Peer().StableID())
|
||||
|
||||
var offset int64
|
||||
@ -138,7 +142,7 @@ func handlePeerPutWithBackend(h ipnlocal.PeerAPIHandler, lb localBackend, w http
|
||||
n, err := taildropMgr.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
|
||||
switch err {
|
||||
case nil:
|
||||
d := lb.Clock().Since(t0).Round(time.Second / 10)
|
||||
d := ext.Clock().Since(t0).Round(time.Second / 10)
|
||||
h.Logf("got put of %s in %v from %v/%v", approxSize(n), d, h.RemoteAddr().Addr(), h.Peer().ComputedName)
|
||||
io.WriteString(w, "{}\n")
|
||||
case taildrop.ErrNoTaildrop:
|
||||
|
@ -50,19 +50,19 @@ func (h *peerAPIHandler) PeerCaps() tailcfg.PeerCapMap {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeLocalBackend struct {
|
||||
type fakeExtension struct {
|
||||
logf logger.Logf
|
||||
capFileSharing bool
|
||||
clock tstime.Clock
|
||||
taildrop *taildrop.Manager
|
||||
}
|
||||
|
||||
func (lb *fakeLocalBackend) Clock() tstime.Clock { return lb.clock }
|
||||
func (lb *fakeLocalBackend) HasCapFileSharing() bool {
|
||||
return lb.capFileSharing
|
||||
func (lb *fakeExtension) manager() *taildrop.Manager {
|
||||
return lb.taildrop
|
||||
}
|
||||
func (lb *fakeLocalBackend) TaildropManager() (*taildrop.Manager, error) {
|
||||
return lb.taildrop, nil
|
||||
func (lb *fakeExtension) Clock() tstime.Clock { return lb.clock }
|
||||
func (lb *fakeExtension) hasCapFileSharing() bool {
|
||||
return lb.capFileSharing
|
||||
}
|
||||
|
||||
type peerAPITestEnv struct {
|
||||
@ -472,16 +472,17 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil}
|
||||
}
|
||||
var rootDir string
|
||||
var e peerAPITestEnv
|
||||
if !tt.omitRoot {
|
||||
rootDir = t.TempDir()
|
||||
e.taildrop = taildrop.ManagerOptions{
|
||||
Logf: e.logBuf.Logf,
|
||||
Dir: rootDir,
|
||||
}.New()
|
||||
}
|
||||
|
||||
lb := &fakeLocalBackend{
|
||||
var e peerAPITestEnv
|
||||
e.taildrop = taildrop.ManagerOptions{
|
||||
Logf: e.logBuf.Logf,
|
||||
Dir: rootDir,
|
||||
}.New()
|
||||
|
||||
ext := &fakeExtension{
|
||||
logf: e.logBuf.Logf,
|
||||
capFileSharing: tt.capSharing,
|
||||
clock: &tstest.Clock{},
|
||||
@ -499,7 +500,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
if req.Host == "example.com" {
|
||||
req.Host = "100.100.100.101:12345"
|
||||
}
|
||||
handlePeerPutWithBackend(e.ph, lb, e.rr, req)
|
||||
handlePeerPutWithBackend(e.ph, ext, e.rr, req)
|
||||
}
|
||||
for _, f := range tt.checks {
|
||||
f(t, &e)
|
||||
@ -539,7 +540,7 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
|
||||
}).View(),
|
||||
}
|
||||
fakeLB := &fakeLocalBackend{
|
||||
fakeLB := &fakeExtension{
|
||||
logf: t.Logf,
|
||||
capFileSharing: true,
|
||||
clock: &tstest.Clock{},
|
||||
|
73
feature/taildrop/target_test.go
Normal file
73
feature/taildrop/target_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestFileTargets(t *testing.T) {
|
||||
e := new(Extension)
|
||||
|
||||
_, err := e.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected to the tailnet"; got != want {
|
||||
t.Errorf("before connect: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
e.nodeBackendForTest = testNodeBackend{peers: nil}
|
||||
|
||||
_, err = e.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)
|
||||
}
|
||||
|
||||
e.backendState = ipn.Running
|
||||
_, err = e.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)
|
||||
}
|
||||
|
||||
e.capFileSharing = true
|
||||
got, err := e.FileTargets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
|
||||
var nodeID tailcfg.NodeID = 1234
|
||||
peer := &tailcfg.Node{
|
||||
ID: nodeID,
|
||||
Hostinfo: (&tailcfg.Hostinfo{OS: "tvOS"}).View(),
|
||||
}
|
||||
e.nodeBackendForTest = testNodeBackend{peers: []tailcfg.NodeView{peer.View()}}
|
||||
|
||||
got, err = e.FileTargets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
type testNodeBackend struct {
|
||||
ipnext.NodeBackend
|
||||
peers []tailcfg.NodeView
|
||||
}
|
||||
|
||||
func (t testNodeBackend) AppendMatchingPeers(peers []tailcfg.NodeView, f func(tailcfg.NodeView) bool) []tailcfg.NodeView {
|
||||
for _, p := range t.peers {
|
||||
if f(p) {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
return peers
|
||||
}
|
@ -9,11 +9,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
@ -191,6 +194,10 @@ type Host interface {
|
||||
// SendNotifyAsync sends a notification to the IPN bus,
|
||||
// typically to the GUI client.
|
||||
SendNotifyAsync(ipn.Notify)
|
||||
|
||||
// NodeBackend returns the [NodeBackend] for the currently active node
|
||||
// (which is approximately the same as the current profile).
|
||||
NodeBackend() NodeBackend
|
||||
}
|
||||
|
||||
// SafeBackend is a subset of the [ipnlocal.LocalBackend] type's methods that
|
||||
@ -323,7 +330,9 @@ type NewControlClientCallback func(controlclient.Client, ipn.LoginProfileView) (
|
||||
// Each hook has its own rules about when it's called and what environment it
|
||||
// has access to and what it's allowed to do.
|
||||
type Hooks struct {
|
||||
// ProfileStateChange are callbacks that are invoked when the current login profile
|
||||
// BackendStateChange is called when the backend state changes.
|
||||
BackendStateChange feature.Hooks[func(ipn.State)]
|
||||
|
||||
// or its [ipn.Prefs] change, after those changes have been made. The current login profile
|
||||
// may be changed either because of a profile switch, or because the profile information
|
||||
// was updated by [LocalBackend.SetControlClientStatus], including when the profile
|
||||
@ -347,4 +356,42 @@ type Hooks struct {
|
||||
// NewControlClient are the functions to be called when a new control client
|
||||
// is created. It is called with the LocalBackend locked.
|
||||
NewControlClient feature.Hooks[NewControlClientCallback]
|
||||
|
||||
// OnSelfChange is called (with LocalBackend.mu held) when the self node
|
||||
// changes, including changing to nothing (an invalid view).
|
||||
OnSelfChange feature.Hooks[func(tailcfg.NodeView)]
|
||||
|
||||
// MutateNotifyLocked is called to optionally mutate the provided Notify
|
||||
// before sending it to the IPN bus. It is called with LocalBackend.mu held.
|
||||
MutateNotifyLocked feature.Hooks[func(*ipn.Notify)]
|
||||
|
||||
// SetPeerStatus is called to mutate PeerStatus.
|
||||
// Callers must only use NodeBackend to read data.
|
||||
SetPeerStatus feature.Hooks[func(*ipnstate.PeerStatus, tailcfg.NodeView, NodeBackend)]
|
||||
}
|
||||
|
||||
// NodeBackend is an interface to query the current node and its peers.
|
||||
//
|
||||
// It is not a snapshot in time but is locked to a particular node.
|
||||
type NodeBackend interface {
|
||||
// AppendMatchingPeers appends all peers that match the predicate
|
||||
// to the base slice and returns it.
|
||||
AppendMatchingPeers(base []tailcfg.NodeView, pred func(tailcfg.NodeView) bool) []tailcfg.NodeView
|
||||
|
||||
// PeerCaps returns the capabilities that src has to this node.
|
||||
PeerCaps(src netip.Addr) tailcfg.PeerCapMap
|
||||
|
||||
// PeerHasCap reports whether the peer has the specified peer capability.
|
||||
PeerHasCap(peer tailcfg.NodeView, cap tailcfg.PeerCapability) bool
|
||||
|
||||
// PeerAPIBase returns the "http://ip:port" URL base to reach peer's
|
||||
// PeerAPI, or the empty string if the peer is invalid or doesn't support
|
||||
// PeerAPI.
|
||||
PeerAPIBase(tailcfg.NodeView) string
|
||||
|
||||
// PeerHasPeerAPI whether the provided peer supports PeerAPI.
|
||||
//
|
||||
// It effectively just reports whether PeerAPIBase(node) is non-empty, but
|
||||
// potentially more efficiently.
|
||||
PeerHasPeerAPI(tailcfg.NodeView) bool
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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})
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -148,6 +148,14 @@ func (nm *NetworkMap) GetIPVIPServiceMap() IPServiceMappings {
|
||||
return res
|
||||
}
|
||||
|
||||
// SelfNodeOrZero returns the self node, or a zero value if nm is nil.
|
||||
func (nm *NetworkMap) SelfNodeOrZero() tailcfg.NodeView {
|
||||
if nm == nil {
|
||||
return tailcfg.NodeView{}
|
||||
}
|
||||
return nm.SelfNode
|
||||
}
|
||||
|
||||
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
|
||||
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
|
||||
for _, p := range nm.Peers {
|
||||
|
Loading…
x
Reference in New Issue
Block a user