mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-15 19:29:08 +02:00
The file drop server listened on 41421, which falls inside the ephemeral port range on Linux (32768-60999) and Windows (49152-65535), so any outbound connection could take it after boot. The receiver then bound a dynamic port and advertised it over signaling, and the sender waited for that advertisement before retrying. That coupled a data plane feature to signaling traffic: once a peer connection is established there is no reason for another offer or answer to go out, so a sender could wait out the grace period for an advertisement that never came. Move the port to 22042, next to the SSH (22022) and DNS forwarder (22054) ports and clear of both ephemeral ranges, and keep the tunnel side fixed the way SSH does. A receiver that cannot bind it falls back to a dynamic port and redirects 22042 to it with an inbound DNAT rule, so senders always dial the well known port and never negotiate. NB_FILEDROP_PORT overrides the local bind only. The DNAT runs ahead of the filter on every backend (nftables prerouting at NAT dest priority, iptables nat/PREROUTING, and the userspace filter's translate-then-redecode path), so the netstack service registry keeps taking the bound port. This drops the port registry, the retry that waited on it, and the signaling plumbing that fed it.
206 lines
5.0 KiB
Go
206 lines
5.0 KiB
Go
package filedrop
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// The transfer directions.
|
|
const (
|
|
DirectionReceived Direction = iota
|
|
DirectionSent
|
|
)
|
|
|
|
// The failure reasons a transfer can end with; None accompanies every other state.
|
|
const (
|
|
ReasonNone FailureReason = iota
|
|
// ReasonUnreachable marks a transport-level failure: nothing listens on the
|
|
// peer's file drop port, so the client is old or the tunnel is down.
|
|
ReasonUnreachable
|
|
// ReasonInterrupted marks a transfer that was still moving when the process
|
|
// stopped; nothing survived to finish or resume it.
|
|
ReasonInterrupted
|
|
)
|
|
|
|
const historyCap = 30
|
|
|
|
// Direction tells whether a transfer was sent by this device or received on it.
|
|
type Direction uint8
|
|
|
|
// FailureReason classifies why a transfer failed, when it is known.
|
|
type FailureReason uint8
|
|
|
|
// String implements fmt.Stringer.
|
|
func (d Direction) String() string {
|
|
switch d {
|
|
case DirectionReceived:
|
|
return "received"
|
|
case DirectionSent:
|
|
return "sent"
|
|
default:
|
|
return fmt.Sprintf("unknown(%d)", uint8(d))
|
|
}
|
|
}
|
|
|
|
// Transfer is one history entry: a sent or received offer with its outcome.
|
|
type Transfer struct {
|
|
ID OfferID `json:"id"`
|
|
Direction Direction `json:"direction"`
|
|
PeerKey PeerKey `json:"peerKey"`
|
|
PeerName string `json:"peerName"`
|
|
Files []FileMeta `json:"files"`
|
|
State State `json:"state"`
|
|
Transferred int64 `json:"transferred"`
|
|
TotalSize int64 `json:"totalSize"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
DeliveredPaths []string `json:"deliveredPaths,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Reason FailureReason `json:"reason,omitempty"`
|
|
}
|
|
|
|
// History is the persisted transfer log of one profile, newest first.
|
|
type History struct {
|
|
mu sync.RWMutex
|
|
store Store
|
|
entries []Transfer
|
|
}
|
|
|
|
// LoadHistory reads the stored transfer log, starting empty when unreadable.
|
|
func LoadHistory(store Store) *History {
|
|
h := &History{store: store}
|
|
|
|
var entries []Transfer
|
|
if err := loadSection(store, namespaceHistory, &entries); err != nil {
|
|
log.Warnf("failed to read file drop history, starting empty: %v", err)
|
|
return h
|
|
}
|
|
h.entries = entries
|
|
if h.settleInterrupted() {
|
|
h.persist()
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (t Transfer) terminal() bool {
|
|
switch t.State {
|
|
case StateCompleted, StateDeclined, StateExpired, StateCancelled, StateFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (t Transfer) clone() Transfer {
|
|
c := t
|
|
c.Files = slices.Clone(t.Files)
|
|
c.DeliveredPaths = slices.Clone(t.DeliveredPaths)
|
|
return c
|
|
}
|
|
|
|
// Upsert inserts or replaces the entry with the same ID and persists the log.
|
|
func (h *History) Upsert(t Transfer) {
|
|
t.UpdatedAt = time.Now()
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if i := h.indexOf(t.ID); i >= 0 {
|
|
h.entries[i] = t
|
|
} else {
|
|
h.entries = slices.Insert(h.entries, 0, t)
|
|
h.prune()
|
|
}
|
|
h.persist()
|
|
}
|
|
|
|
// SetProgress updates the transferred byte count in memory only.
|
|
func (h *History) SetProgress(id OfferID, transferred int64) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if i := h.indexOf(id); i >= 0 {
|
|
h.entries[i].Transferred = transferred
|
|
h.entries[i].UpdatedAt = time.Now()
|
|
if h.entries[i].State == StatePending {
|
|
h.entries[i].State = StateTransferring
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get returns the entry with the given ID.
|
|
func (h *History) Get(id OfferID) (Transfer, bool) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
if i := h.indexOf(id); i >= 0 {
|
|
return h.entries[i].clone(), true
|
|
}
|
|
return Transfer{}, false
|
|
}
|
|
|
|
// List returns every entry, newest first.
|
|
func (h *History) List() []Transfer {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
list := make([]Transfer, len(h.entries))
|
|
for i, e := range h.entries {
|
|
list[i] = e.clone()
|
|
}
|
|
return list
|
|
}
|
|
|
|
// Delete removes one entry and persists the log.
|
|
func (h *History) Delete(id OfferID) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if i := h.indexOf(id); i >= 0 {
|
|
h.entries = slices.Delete(h.entries, i, i+1)
|
|
h.persist()
|
|
}
|
|
}
|
|
|
|
func (h *History) indexOf(id OfferID) int {
|
|
return slices.IndexFunc(h.entries, func(t Transfer) bool { return t.ID == id })
|
|
}
|
|
|
|
func (h *History) prune() {
|
|
if len(h.entries) <= historyCap {
|
|
return
|
|
}
|
|
for i := len(h.entries) - 1; i >= 0 && len(h.entries) > historyCap; i-- {
|
|
if h.entries[i].terminal() {
|
|
h.entries = slices.Delete(h.entries, i, i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// settleInterrupted closes out transfers that were still moving when the
|
|
// process died. Nothing is left to finish them, so left alone they would sit in
|
|
// the log as permanently pending. Reports whether anything changed.
|
|
func (h *History) settleInterrupted() bool {
|
|
changed := false
|
|
for i, t := range h.entries {
|
|
if t.terminal() {
|
|
continue
|
|
}
|
|
h.entries[i].State = StateFailed
|
|
h.entries[i].Reason = ReasonInterrupted
|
|
h.entries[i].UpdatedAt = time.Now()
|
|
changed = true
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func (h *History) persist() {
|
|
if err := saveSection(h.store, namespaceHistory, h.entries); err != nil {
|
|
log.Warnf("failed to write file drop history: %v", err)
|
|
}
|
|
}
|