mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
The receiver's host listener is a socket of the Network Extension, so the SYN-ACK of an accepted connection followed the extension's own-traffic bypass onto the physical interface and the sender never got an answer. Bind the listeners to the tunnel interface index the same way the dial already is; accepted sockets inherit the scope. Other platforms pass a nil control and keep the current behavior.
619 lines
15 KiB
Go
619 lines
15 KiB
Go
package filedrop
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
)
|
|
|
|
// The event kinds. Progress is one of them, thinned to a few reports a second
|
|
// by the reader that raises it; see progressInterval.
|
|
const (
|
|
EventOffer EventKind = iota
|
|
EventCompleted
|
|
EventFailed
|
|
EventWithdrawn
|
|
EventProgress
|
|
)
|
|
|
|
// ErrNotConnected indicates the operation needs a running tunnel.
|
|
var ErrNotConnected = errors.New("not connected")
|
|
|
|
// EventKind classifies the events the manager surfaces to the platform layer.
|
|
type EventKind uint8
|
|
|
|
// EventSink receives transfer events. Calls may come from server goroutines.
|
|
type EventSink func(kind EventKind, transfer Transfer)
|
|
|
|
// ManagerConfig configures a per-profile file drop manager. Policy and history
|
|
// live in Store; DataDir only holds the spool of partially received files,
|
|
// which is disposable and never outlives an offer's TTL.
|
|
type ManagerConfig struct {
|
|
Profile profilemanager.ID
|
|
DataDir string
|
|
Store Store
|
|
Events EventSink
|
|
OfferTTL time.Duration
|
|
Sink Sink
|
|
}
|
|
|
|
type sendHandle struct {
|
|
cancel context.CancelFunc
|
|
ip netip.Addr
|
|
addr netip.AddrPort
|
|
remoteID OfferID
|
|
}
|
|
|
|
// Manager owns one profile's file drop state.
|
|
type Manager struct {
|
|
mu sync.Mutex
|
|
profile profilemanager.ID
|
|
dataDir string
|
|
store Store
|
|
policy *PolicyStore
|
|
history *History
|
|
events EventSink
|
|
offerTTL time.Duration
|
|
sink Sink
|
|
|
|
server *Server
|
|
dial DialFunc
|
|
senderName string
|
|
sends map[OfferID]*sendHandle
|
|
sendWg sync.WaitGroup
|
|
}
|
|
|
|
// NewManager loads or initializes the file drop state for one profile.
|
|
func NewManager(cfg ManagerConfig) (*Manager, error) {
|
|
if cfg.DataDir == "" {
|
|
return nil, errors.New("data dir is required")
|
|
}
|
|
if err := os.MkdirAll(cfg.DataDir, 0o700); err != nil {
|
|
return nil, fmt.Errorf("create file drop dir: %w", err)
|
|
}
|
|
|
|
m := &Manager{
|
|
profile: cfg.Profile,
|
|
dataDir: cfg.DataDir,
|
|
store: cfg.Store,
|
|
policy: LoadPolicyStore(cfg.Profile, cfg.Store),
|
|
history: LoadHistory(cfg.Store),
|
|
events: cfg.Events,
|
|
offerTTL: cfg.OfferTTL,
|
|
sink: cfg.Sink,
|
|
sends: make(map[OfferID]*sendHandle),
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// Profile returns the profile this manager belongs to.
|
|
func (m *Manager) Profile() profilemanager.ID {
|
|
return m.profile
|
|
}
|
|
|
|
// Policy returns the receiving policy store.
|
|
func (m *Manager) Policy() *PolicyStore {
|
|
return m.policy
|
|
}
|
|
|
|
// ReceiverPort returns the port the receiver is actually bound to, 0 when stopped.
|
|
func (m *Manager) ReceiverPort() uint16 {
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.mu.Unlock()
|
|
if server == nil {
|
|
return 0
|
|
}
|
|
return server.BoundPort()
|
|
}
|
|
|
|
// Transfers returns the history entries, newest first, with pending offers included.
|
|
func (m *Manager) Transfers() []Transfer {
|
|
return m.history.List()
|
|
}
|
|
|
|
// DeleteTransfer removes a history entry. A live transfer is cancelled first.
|
|
func (m *Manager) DeleteTransfer(id OfferID) {
|
|
if t, ok := m.history.Get(id); ok && !t.terminal() {
|
|
m.Cancel(id)
|
|
}
|
|
m.history.Delete(id)
|
|
}
|
|
|
|
// DestinationDir returns where received files are delivered: the platform
|
|
// sink's own name for it when one stages payloads, otherwise the configured
|
|
// directory.
|
|
func (m *Manager) DestinationDir() string {
|
|
if labeller, ok := m.sink.(interface{ DestinationLabel() string }); ok {
|
|
return labeller.DestinationLabel()
|
|
}
|
|
return m.policy.DestinationDir()
|
|
}
|
|
|
|
// SetDestinationDir persists the delivery directory.
|
|
func (m *Manager) SetDestinationDir(dir string) error {
|
|
return m.policy.SetDestinationDir(dir)
|
|
}
|
|
|
|
// StartReceiver binds the receiving server on addr. A non-nil control is
|
|
// applied to host listeners before bind.
|
|
func (m *Manager) StartReceiver(ctx context.Context, addr netip.AddrPort, netstackNet *netstack.Net, resolver PeerResolver, control ListenControl) error {
|
|
m.mu.Lock()
|
|
if m.server != nil {
|
|
m.mu.Unlock()
|
|
return errors.New("receiver is already running")
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
server, err := NewServer(ServerConfig{
|
|
SpoolDir: filepath.Join(m.dataDir, "spool"),
|
|
Sink: m.sink,
|
|
Policy: m.policy,
|
|
Resolver: resolver,
|
|
Notifier: m,
|
|
OfferTTL: m.offerTTL,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("create receiver: %w", err)
|
|
}
|
|
if netstackNet != nil {
|
|
server.SetNetstackNet(netstackNet)
|
|
}
|
|
if control != nil {
|
|
server.SetListenControl(control)
|
|
}
|
|
|
|
if err := server.Start(ctx, addr); err != nil {
|
|
return fmt.Errorf("start receiver: %w", err)
|
|
}
|
|
|
|
m.mu.Lock()
|
|
m.server = server
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// AddReceiverListener serves the receiver on an additional address, such as IPv6.
|
|
func (m *Manager) AddReceiverListener(ctx context.Context, addr netip.AddrPort) error {
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.mu.Unlock()
|
|
|
|
if server == nil {
|
|
return errors.New("receiver is not running")
|
|
}
|
|
return server.AddListener(ctx, addr)
|
|
}
|
|
|
|
// StopReceiver shuts the receiving server down and drops the tunnel dialer.
|
|
func (m *Manager) StopReceiver() error {
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.server = nil
|
|
m.dial = nil
|
|
m.mu.Unlock()
|
|
|
|
if server == nil {
|
|
return nil
|
|
}
|
|
return server.Stop()
|
|
}
|
|
|
|
// SetTunnel gives the manager the tunnel dialer and the local sender name.
|
|
func (m *Manager) SetTunnel(dial DialFunc, senderName string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.dial = dial
|
|
m.senderName = senderName
|
|
}
|
|
|
|
// Close stops the receiver and aborts every outgoing transfer.
|
|
func (m *Manager) Close() error {
|
|
err := m.StopReceiver()
|
|
|
|
m.mu.Lock()
|
|
for _, h := range m.sends {
|
|
h.cancel()
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
m.sendWg.Wait()
|
|
return err
|
|
}
|
|
|
|
// Send starts an asynchronous transfer and returns its local transfer ID.
|
|
func (m *Manager) Send(peer PeerKey, peerName string, addr netip.Addr, payloads []Payload) (OfferID, error) {
|
|
if len(payloads) == 0 {
|
|
return "", fmt.Errorf("%w: no payloads", ErrInvalidOffer)
|
|
}
|
|
|
|
m.mu.Lock()
|
|
dial, senderName := m.dial, m.senderName
|
|
m.mu.Unlock()
|
|
if dial == nil {
|
|
return "", ErrNotConnected
|
|
}
|
|
|
|
client, err := NewClient(ClientConfig{Dial: dial, SenderName: senderName, OfferTimeout: m.offerTTL})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
id := OfferID(uuid.NewString())
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
handle := &sendHandle{cancel: cancel, ip: addr}
|
|
|
|
m.mu.Lock()
|
|
m.sends[id] = handle
|
|
m.mu.Unlock()
|
|
|
|
transfer := Transfer{
|
|
ID: id,
|
|
Direction: DirectionSent,
|
|
PeerKey: peer,
|
|
PeerName: peerName,
|
|
Files: payloadMetas(payloads),
|
|
State: StatePending,
|
|
TotalSize: payloadTotal(payloads),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
m.history.Upsert(transfer)
|
|
|
|
m.sendWg.Add(1)
|
|
go func() {
|
|
defer m.sendWg.Done()
|
|
defer cancel()
|
|
m.runSend(ctx, client, handle, transfer, payloads)
|
|
|
|
m.mu.Lock()
|
|
delete(m.sends, id)
|
|
m.mu.Unlock()
|
|
}()
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// Cancel aborts a transfer in either direction.
|
|
func (m *Manager) Cancel(id OfferID) {
|
|
m.mu.Lock()
|
|
handle := m.sends[id]
|
|
server := m.server
|
|
var remoteAddr netip.AddrPort
|
|
var remoteID OfferID
|
|
if handle != nil {
|
|
remoteAddr, remoteID = handle.addr, handle.remoteID
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
if handle != nil {
|
|
handle.cancel()
|
|
if remoteID != "" && remoteAddr.IsValid() {
|
|
m.withdrawRemote(remoteAddr, remoteID)
|
|
}
|
|
m.finishTransfer(id, StateCancelled, "")
|
|
return
|
|
}
|
|
|
|
if server != nil {
|
|
if offer, ok := server.Offers().Revoke(id); ok {
|
|
server.Spool().Remove(offer.ID)
|
|
}
|
|
}
|
|
m.finishTransfer(id, StateCancelled, "")
|
|
}
|
|
|
|
// Accept releases a pending incoming offer for upload.
|
|
func (m *Manager) Accept(id OfferID) error {
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.mu.Unlock()
|
|
if server == nil {
|
|
return ErrNotConnected
|
|
}
|
|
|
|
offer, ok := server.Offers().Decide(id, DecisionAccepted)
|
|
if !ok {
|
|
return ErrOfferNotFound
|
|
}
|
|
|
|
if offer.State == StateCompleted {
|
|
m.OnCompleted(offer)
|
|
return nil
|
|
}
|
|
|
|
m.history.SetProgress(id, 0)
|
|
return nil
|
|
}
|
|
|
|
// Decline refuses a pending incoming offer.
|
|
func (m *Manager) Decline(id OfferID) error {
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.mu.Unlock()
|
|
if server == nil {
|
|
return ErrNotConnected
|
|
}
|
|
|
|
offer, ok := server.Offers().Decide(id, DecisionDeclined)
|
|
if !ok {
|
|
return ErrOfferNotFound
|
|
}
|
|
|
|
server.Spool().Remove(offer.ID)
|
|
m.finishTransfer(id, StateDeclined, "")
|
|
return nil
|
|
}
|
|
|
|
// SetSenderRule records a per-sender exception.
|
|
func (m *Manager) SetSenderRule(peer PeerKey, rule SenderRule) error {
|
|
if err := m.policy.SetSenderRule(peer, rule); err != nil {
|
|
return err
|
|
}
|
|
if rule != SenderRuleBlock {
|
|
return nil
|
|
}
|
|
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.mu.Unlock()
|
|
if server == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, offer := range server.Offers().List() {
|
|
if offer.Sender != peer || offer.Decision != DecisionPending {
|
|
continue
|
|
}
|
|
if declined, ok := server.Offers().Decide(offer.ID, DecisionDeclined); ok {
|
|
server.Spool().Remove(declined.ID)
|
|
m.finishTransfer(declined.ID, StateDeclined, "")
|
|
m.emit(EventWithdrawn, m.transferOf(declined.ID))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) runSend(ctx context.Context, client *Client, handle *sendHandle, transfer Transfer, payloads []Payload) {
|
|
addr := netip.AddrPortFrom(handle.ip, Port)
|
|
remoteID, decision, err := client.Offer(ctx, addr, payloads)
|
|
if err != nil {
|
|
m.failSend(ctx, transfer.ID, err)
|
|
return
|
|
}
|
|
m.storeRemote(handle, addr, remoteID)
|
|
|
|
decision, err = client.AwaitDecision(ctx, addr, remoteID, decision)
|
|
if err != nil {
|
|
m.failSend(ctx, transfer.ID, err)
|
|
return
|
|
}
|
|
if err := decisionError(decision); err != nil {
|
|
m.failSend(ctx, transfer.ID, err)
|
|
return
|
|
}
|
|
|
|
m.history.SetProgress(transfer.ID, 0)
|
|
|
|
completed := make([]int64, len(payloads))
|
|
progress := func(index int, sent, _ int64) {
|
|
completed[index] = sent
|
|
var total int64
|
|
for _, n := range completed {
|
|
total += n
|
|
}
|
|
m.history.SetProgress(transfer.ID, total)
|
|
m.emit(EventProgress, m.transferOf(transfer.ID))
|
|
}
|
|
|
|
if err := client.Upload(ctx, addr, remoteID, payloads, progress); err != nil {
|
|
m.failSend(ctx, transfer.ID, err)
|
|
return
|
|
}
|
|
|
|
m.history.SetProgress(transfer.ID, transfer.TotalSize)
|
|
m.finishTransfer(transfer.ID, StateCompleted, "")
|
|
m.emit(EventCompleted, m.transferOf(transfer.ID))
|
|
}
|
|
|
|
func (m *Manager) storeRemote(handle *sendHandle, addr netip.AddrPort, remoteID OfferID) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
handle.addr = addr
|
|
handle.remoteID = remoteID
|
|
}
|
|
|
|
func (m *Manager) failSend(ctx context.Context, id OfferID, err error) {
|
|
if ctx.Err() != nil {
|
|
m.finishTransfer(id, StateCancelled, "")
|
|
return
|
|
}
|
|
|
|
state := StateFailed
|
|
switch {
|
|
// Withdrawn mid-transfer reads the same way as refused up front: the
|
|
// receiver said no, which is an answer rather than a failure.
|
|
case errors.Is(err, ErrDeclined), errors.Is(err, ErrNotAccepted),
|
|
errors.Is(err, ErrOfferNotFound):
|
|
state = StateDeclined
|
|
case errors.Is(err, ErrExpired):
|
|
state = StateExpired
|
|
}
|
|
|
|
message := ""
|
|
reason := ReasonNone
|
|
if state == StateFailed {
|
|
message = err.Error()
|
|
if transportFailure(err) {
|
|
reason = ReasonUnreachable
|
|
}
|
|
}
|
|
m.finishTransferReason(id, state, message, reason)
|
|
m.emit(EventFailed, m.transferOf(id))
|
|
}
|
|
|
|
func (m *Manager) withdrawRemote(addr netip.AddrPort, remoteID OfferID) {
|
|
m.mu.Lock()
|
|
dial, senderName := m.dial, m.senderName
|
|
m.mu.Unlock()
|
|
if dial == nil {
|
|
return
|
|
}
|
|
|
|
client, err := NewClient(ClientConfig{Dial: dial, SenderName: senderName})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := client.Cancel(ctx, addr, remoteID); err != nil {
|
|
log.Debugf("failed to withdraw file drop offer: %v", err)
|
|
}
|
|
}
|
|
|
|
// OnOffer implements Notifier for the receiver server.
|
|
func (m *Manager) OnOffer(offer Offer) {
|
|
transfer := Transfer{
|
|
ID: offer.ID,
|
|
Direction: DirectionReceived,
|
|
PeerKey: offer.Sender,
|
|
PeerName: offer.SenderName,
|
|
Files: offer.Files,
|
|
State: offer.State,
|
|
TotalSize: offer.TotalSize(),
|
|
CreatedAt: offer.CreatedAt,
|
|
}
|
|
m.history.Upsert(transfer)
|
|
|
|
if offer.Decision == DecisionPending {
|
|
m.emit(EventOffer, transfer)
|
|
}
|
|
if offer.Decision == DecisionAccepted && offer.State == StateCompleted {
|
|
m.OnCompleted(offer)
|
|
}
|
|
}
|
|
|
|
// OnProgress implements Notifier.
|
|
func (m *Manager) OnProgress(offer Offer, index int, received int64) {
|
|
var total int64
|
|
for i, n := range offer.Progress {
|
|
if i == index {
|
|
n = received
|
|
}
|
|
total += n
|
|
}
|
|
m.history.SetProgress(offer.ID, total)
|
|
m.emit(EventProgress, m.transferOf(offer.ID))
|
|
}
|
|
|
|
// OnCompleted implements Notifier.
|
|
func (m *Manager) OnCompleted(offer Offer) {
|
|
m.mu.Lock()
|
|
server := m.server
|
|
m.mu.Unlock()
|
|
if server == nil {
|
|
return
|
|
}
|
|
|
|
transfer, ok := m.history.Get(offer.ID)
|
|
if !ok || transfer.State == StateCompleted {
|
|
return
|
|
}
|
|
|
|
delivered, err := server.Spool().Deliver(offer, m.policy.DestinationDir())
|
|
if err != nil {
|
|
log.Errorf("failed to deliver file drop payloads: %v", err)
|
|
// Nothing will ask for these payloads again, and a sink that cannot
|
|
// address them by path has no sweep of its own to fall back on.
|
|
server.Spool().Remove(offer.ID)
|
|
m.finishTransfer(offer.ID, StateFailed, err.Error())
|
|
m.emit(EventFailed, m.transferOf(offer.ID))
|
|
return
|
|
}
|
|
|
|
transfer.State = StateCompleted
|
|
transfer.Transferred = transfer.TotalSize
|
|
transfer.DeliveredPaths = delivered
|
|
transfer.Error = ""
|
|
m.history.Upsert(transfer)
|
|
m.emit(EventCompleted, transfer)
|
|
}
|
|
|
|
// OnFailed implements Notifier.
|
|
func (m *Manager) OnFailed(offer Offer, err error) {
|
|
if errors.Is(err, ErrExpired) {
|
|
m.finishTransfer(offer.ID, StateExpired, "")
|
|
m.emit(EventWithdrawn, m.transferOf(offer.ID))
|
|
return
|
|
}
|
|
m.finishTransfer(offer.ID, StateFailed, err.Error())
|
|
m.emit(EventFailed, m.transferOf(offer.ID))
|
|
}
|
|
|
|
// OnWithdrawn implements Notifier: the sender cancelled, so the consent prompt goes away.
|
|
func (m *Manager) OnWithdrawn(offer Offer) {
|
|
m.finishTransfer(offer.ID, StateCancelled, "")
|
|
m.emit(EventWithdrawn, m.transferOf(offer.ID))
|
|
}
|
|
|
|
func (m *Manager) finishTransfer(id OfferID, state State, message string) {
|
|
m.finishTransferReason(id, state, message, ReasonNone)
|
|
}
|
|
|
|
func (m *Manager) finishTransferReason(id OfferID, state State, message string, reason FailureReason) {
|
|
transfer, ok := m.history.Get(id)
|
|
if !ok || transfer.terminal() {
|
|
return
|
|
}
|
|
transfer.State = state
|
|
transfer.Error = message
|
|
transfer.Reason = reason
|
|
m.history.Upsert(transfer)
|
|
}
|
|
|
|
func (m *Manager) transferOf(id OfferID) Transfer {
|
|
t, _ := m.history.Get(id)
|
|
return t
|
|
}
|
|
|
|
func (m *Manager) emit(kind EventKind, transfer Transfer) {
|
|
if m.events != nil && transfer.ID != "" {
|
|
m.events(kind, transfer)
|
|
}
|
|
}
|
|
|
|
func payloadMetas(payloads []Payload) []FileMeta {
|
|
metas := make([]FileMeta, len(payloads))
|
|
for i, p := range payloads {
|
|
metas[i] = p.Meta
|
|
}
|
|
return metas
|
|
}
|
|
|
|
func payloadTotal(payloads []Payload) int64 {
|
|
var total int64
|
|
for _, p := range payloads {
|
|
total += p.Meta.Size
|
|
}
|
|
return total
|
|
}
|
|
|
|
// transportFailure reports whether the offer never reached the receiver; any HTTP
|
|
// response, refusal included, means the receiver answered.
|
|
func transportFailure(err error) bool {
|
|
var urlErr *url.Error
|
|
return errors.As(err, &urlErr)
|
|
}
|