Files
Zoltan Papp ec0659cbf0 [client] Bind the file drop receiver only while receiving is enabled
The receiver was bound on every engine start regardless of the profile's
receiving mode, and a failed bind also left the manager without a tunnel
dialer, which took sending down with it.

Set the tunnel dialer before the bind so sending no longer depends on it,
and gate the bind on the receiving mode. A mode change now notifies the
engine, which binds or unbinds accordingly: turning receiving off releases
the listeners and the offer store, empties the spool and settles unfinished
incoming transfers as cancelled.

Also log file drop send and receive failures with the peer and the step that
broke, so a failure can be read from the client log alone.
2026-09-05 19:58:08 +02:00

234 lines
5.5 KiB
Go

package filedrop
import (
"fmt"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
const (
SenderRuleDefault SenderRule = iota
SenderRuleAlwaysAccept
SenderRuleBlock
)
// SenderRule is a per-sender override on top of the base mode.
type SenderRule uint8
// Policy is the device-local receiving policy of one profile. An empty
// DestinationDir means the platform's default download directory.
type Policy struct {
Mode Mode `json:"mode"`
Senders map[PeerKey]SenderRule `json:"senders,omitempty"`
DestinationDir string `json:"destinationDir,omitempty"`
}
// PolicyStore holds the receiving policy of one profile and evaluates it per sender.
type PolicyStore struct {
mu sync.RWMutex
profile profilemanager.ID
policy Policy
store Store
onChange func()
}
// NewPolicyStore returns an in-memory store seeded with the default policy.
func NewPolicyStore(profile profilemanager.ID) *PolicyStore {
return &PolicyStore{profile: profile, policy: DefaultPolicy()}
}
// LoadPolicyStore builds a store from the persisted policy of one profile.
func LoadPolicyStore(profile profilemanager.ID, store Store) *PolicyStore {
s := &PolicyStore{
profile: profile,
policy: DefaultPolicy(),
store: store,
}
if store == nil {
return s
}
policy := DefaultPolicy()
if err := loadSection(store, namespacePolicy, &policy); err != nil {
log.Warnf("failed to load file drop policy for profile %s, using defaults: %v", profile, err)
return s
}
if err := policy.validate(); err != nil {
log.Warnf("stored file drop policy for profile %s is invalid, using defaults: %v", profile, err)
return s
}
s.policy = policy.normalized()
return s
}
// String implements fmt.Stringer.
func (r SenderRule) String() string {
switch r {
case SenderRuleDefault:
return "default"
case SenderRuleAlwaysAccept:
return "always"
case SenderRuleBlock:
return "block"
default:
return fmt.Sprintf("unknown(%d)", uint8(r))
}
}
func (p Policy) validate() error {
if !p.Mode.valid() {
return fmt.Errorf("invalid mode %s", p.Mode)
}
return nil
}
func (p Policy) normalized() Policy {
c := p.clone()
if c.Senders == nil {
c.Senders = map[PeerKey]SenderRule{}
}
return c
}
func (p Policy) clone() Policy {
c := p
c.Senders = make(map[PeerKey]SenderRule, len(p.Senders))
for k, v := range p.Senders {
c.Senders[k] = v
}
return c
}
// Profile returns the profile this policy belongs to.
func (s *PolicyStore) Profile() profilemanager.ID {
return s.profile
}
// SetChangeHandler registers a handler invoked when the base mode changes, so
// the owner of the receiver can bind or unbind it. It runs on the goroutine
// that made the change, with no store lock held.
func (s *PolicyStore) SetChangeHandler(h func()) {
s.mu.Lock()
s.onChange = h
s.mu.Unlock()
}
// Receiving reports whether the profile accepts incoming offers at all.
func (s *PolicyStore) Receiving() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.policy.Mode != ModeOff
}
// Get returns a copy of the current policy.
func (s *PolicyStore) Get() Policy {
s.mu.RLock()
defer s.mu.RUnlock()
return s.policy.clone()
}
// Set replaces the policy and persists it.
func (s *PolicyStore) Set(p Policy) error {
if err := p.validate(); err != nil {
return err
}
s.mu.Lock()
was := s.policy.Mode
s.policy = p.normalized()
store, stored, changed, notify := s.store, s.policy.clone(), was != s.policy.Mode, s.onChange
s.mu.Unlock()
err := saveSection(store, namespacePolicy, stored)
if changed && notify != nil {
notify()
}
return err
}
// SetMode changes the base mode, leaving per-sender rules untouched.
func (s *PolicyStore) SetMode(m Mode) error {
if !m.valid() {
return fmt.Errorf("invalid mode %s", m)
}
s.mu.Lock()
changed := s.policy.Mode != m
s.policy.Mode = m
store, stored, notify := s.store, s.policy.clone(), s.onChange
s.mu.Unlock()
err := saveSection(store, namespacePolicy, stored)
if changed && notify != nil {
notify()
}
return err
}
// SetSenderRule sets or clears the override for a single sender.
func (s *PolicyStore) SetSenderRule(key PeerKey, rule SenderRule) error {
s.mu.Lock()
if rule == SenderRuleDefault {
delete(s.policy.Senders, key)
} else {
if s.policy.Senders == nil {
s.policy.Senders = map[PeerKey]SenderRule{}
}
s.policy.Senders[key] = rule
}
store, stored := s.store, s.policy.clone()
s.mu.Unlock()
return saveSection(store, namespacePolicy, stored)
}
// DestinationDir returns the directory received files are delivered to.
func (s *PolicyStore) DestinationDir() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.policy.DestinationDir
}
// SetDestinationDir persists the delivery directory.
func (s *PolicyStore) SetDestinationDir(dir string) error {
s.mu.Lock()
s.policy.DestinationDir = dir
store, stored := s.store, s.policy.clone()
s.mu.Unlock()
return saveSection(store, namespacePolicy, stored)
}
// Evaluate returns the mode that applies to one sender, denying on unknown values.
func (s *PolicyStore) Evaluate(key PeerKey) Mode {
s.mu.RLock()
defer s.mu.RUnlock()
switch s.policy.Senders[key] {
case SenderRuleBlock:
return ModeOff
case SenderRuleAlwaysAccept:
return ModeAutoAccept
case SenderRuleDefault:
default:
return ModeOff
}
if !s.policy.Mode.valid() {
return ModeOff
}
return s.policy.Mode
}
// DefaultPolicy asks before accepting anything, so receiving is never silently on.
func DefaultPolicy() Policy {
return Policy{
Mode: ModeAsk,
Senders: map[PeerKey]SenderRule{},
}
}