mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 00:51:28 +02:00
Files move directly between peers over the overlay, with no server in the path. The receiver listens on the WireGuard address only, so the port is unreachable from outside the tunnel, and every offer is matched to a known peer before anything is read. Consent is the default: an offer carries metadata alone, and no payload moves until the receiver accepts. Policy is per profile and device-local — off, ask, or auto-accept, with per-sender exceptions on top. Policy and history live in the profile's preferences, so removing a profile takes its file drop state with it. Transfers interrupted by a restart are settled on load; nothing survives to finish them, and left alone they would sit in the log as permanently pending. The Android bindings pull payload bytes through a chunk-returning stream: gomobile copies a []byte argument into a fresh Java array and never copies it back, so a fill-my-buffer method would hand back the right length with no data.
263 lines
6.4 KiB
Go
263 lines
6.4 KiB
Go
package filedrop
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
)
|
|
|
|
const (
|
|
defaultSpoolMaxAge = 24 * time.Hour
|
|
janitorInterval = 10 * time.Minute
|
|
readHeaderTimeout = 30 * time.Second
|
|
idleTimeout = 5 * time.Minute
|
|
)
|
|
|
|
// PeerResolver maps the source overlay address of a connection to the peer that owns it.
|
|
type PeerResolver interface {
|
|
ResolvePeer(addr netip.Addr) (key PeerKey, name string, ok bool)
|
|
}
|
|
|
|
// Notifier receives receiver-side transfer events for the platform layer to surface.
|
|
type Notifier interface {
|
|
OnOffer(offer Offer)
|
|
OnProgress(offer Offer, index int, received int64)
|
|
OnCompleted(offer Offer)
|
|
OnFailed(offer Offer, err error)
|
|
OnWithdrawn(offer Offer)
|
|
}
|
|
|
|
// ServerConfig configures the receiving side.
|
|
type ServerConfig struct {
|
|
SpoolDir string
|
|
Policy *PolicyStore
|
|
Resolver PeerResolver
|
|
Notifier Notifier
|
|
OfferTTL time.Duration
|
|
SpoolMaxAge time.Duration
|
|
}
|
|
|
|
// Server serves the receiver over HTTP on the overlay address and owns the
|
|
// listener and janitor lifecycle; the protocol logic itself lives in receiver.
|
|
type Server struct {
|
|
mu sync.RWMutex
|
|
httpServer *http.Server
|
|
listener net.Listener
|
|
extraListeners []net.Listener
|
|
netstackNet *netstack.Net
|
|
|
|
recv *receiver
|
|
boundPort uint16
|
|
|
|
janitorStop context.CancelFunc
|
|
janitorDone chan struct{}
|
|
}
|
|
|
|
// NewServer builds a receiving server. It does not start listening.
|
|
func NewServer(cfg ServerConfig) (*Server, error) {
|
|
if cfg.Resolver == nil {
|
|
return nil, errors.New("peer resolver is required")
|
|
}
|
|
if cfg.Policy == nil {
|
|
return nil, errors.New("receiving policy is required")
|
|
}
|
|
|
|
spool, err := NewSpool(cfg.SpoolDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create spool: %w", err)
|
|
}
|
|
|
|
maxAge := cfg.SpoolMaxAge
|
|
if maxAge <= 0 {
|
|
maxAge = defaultSpoolMaxAge
|
|
}
|
|
|
|
return &Server{recv: newReceiver(cfg, spool, maxAge)}, nil
|
|
}
|
|
|
|
// SetNetstackNet routes listeners through the gVisor netstack instead of host sockets.
|
|
func (s *Server) SetNetstackNet(n *netstack.Net) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.netstackNet = n
|
|
}
|
|
|
|
// Offers returns the offer store, for the platform layer to accept, decline, and list.
|
|
func (s *Server) Offers() *OfferStore {
|
|
return s.recv.offers
|
|
}
|
|
|
|
// Spool returns the staging area, so the platform layer can deliver completed payloads.
|
|
func (s *Server) Spool() *Spool {
|
|
return s.recv.spool
|
|
}
|
|
|
|
// Policy returns the active profile's receiving policy store.
|
|
func (s *Server) Policy() *PolicyStore {
|
|
return s.recv.policy
|
|
}
|
|
|
|
// BoundPort returns the port the server actually listens on, 0 when stopped.
|
|
func (s *Server) BoundPort() uint16 {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.boundPort
|
|
}
|
|
|
|
// Start binds the service to addr and serves until Stop.
|
|
func (s *Server) Start(ctx context.Context, addr netip.AddrPort) error {
|
|
s.mu.Lock()
|
|
if s.httpServer != nil {
|
|
s.mu.Unlock()
|
|
return errors.New("file drop server is already running")
|
|
}
|
|
|
|
ln, desc, err := s.createListener(ctx, addr)
|
|
if err != nil && addr.Port() != 0 {
|
|
log.Warnf("file drop port %d is unavailable, falling back to a dynamic port: %v", addr.Port(), err)
|
|
ln, desc, err = s.createListener(ctx, netip.AddrPortFrom(addr.Addr(), 0))
|
|
}
|
|
if err != nil {
|
|
s.mu.Unlock()
|
|
return fmt.Errorf("create listener: %w", err)
|
|
}
|
|
|
|
transport := &httpTransport{recv: s.recv}
|
|
httpServer := &http.Server{
|
|
Handler: transport.routes(),
|
|
ReadHeaderTimeout: readHeaderTimeout,
|
|
IdleTimeout: idleTimeout,
|
|
}
|
|
|
|
janitorCtx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
|
|
s.listener = ln
|
|
s.httpServer = httpServer
|
|
s.boundPort = listenerPort(ln, addr.Port())
|
|
s.janitorStop = cancel
|
|
s.janitorDone = done
|
|
s.mu.Unlock()
|
|
|
|
go s.runJanitor(janitorCtx, done)
|
|
go s.serve(httpServer, ln, desc)
|
|
|
|
log.Infof("file drop server started on %s", desc)
|
|
return nil
|
|
}
|
|
|
|
// AddListener serves the running service on an additional address, such as IPv6.
|
|
func (s *Server) AddListener(ctx context.Context, addr netip.AddrPort) error {
|
|
s.mu.Lock()
|
|
httpServer := s.httpServer
|
|
if httpServer == nil {
|
|
s.mu.Unlock()
|
|
return errors.New("file drop server is not running")
|
|
}
|
|
|
|
ln, desc, err := s.createListener(ctx, addr)
|
|
if err != nil {
|
|
s.mu.Unlock()
|
|
return fmt.Errorf("create listener: %w", err)
|
|
}
|
|
s.extraListeners = append(s.extraListeners, ln)
|
|
s.mu.Unlock()
|
|
|
|
go s.serve(httpServer, ln, desc)
|
|
|
|
log.Infof("file drop server also listening on %s", desc)
|
|
return nil
|
|
}
|
|
|
|
// Stop shuts the service down and releases the offers it was tracking. It is idempotent.
|
|
func (s *Server) Stop() error {
|
|
s.mu.Lock()
|
|
httpServer := s.httpServer
|
|
if httpServer == nil {
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
s.httpServer = nil
|
|
s.listener = nil
|
|
s.boundPort = 0
|
|
extra := s.extraListeners
|
|
s.extraListeners = nil
|
|
stopJanitor, janitorDone := s.janitorStop, s.janitorDone
|
|
s.janitorStop, s.janitorDone = nil, nil
|
|
s.mu.Unlock()
|
|
|
|
if stopJanitor != nil {
|
|
stopJanitor()
|
|
<-janitorDone
|
|
}
|
|
|
|
err := httpServer.Close()
|
|
|
|
for _, ln := range extra {
|
|
if cerr := ln.Close(); cerr != nil {
|
|
log.Debugf("close extra file drop listener: %v", cerr)
|
|
}
|
|
}
|
|
|
|
s.recv.close()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("close: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) serve(httpServer *http.Server, ln net.Listener, desc string) {
|
|
if err := httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Errorf("file drop server error on %s: %v", desc, err)
|
|
}
|
|
}
|
|
|
|
func (s *Server) createListener(ctx context.Context, addr netip.AddrPort) (net.Listener, string, error) {
|
|
if s.netstackNet != nil {
|
|
ln, err := s.netstackNet.ListenTCPAddrPort(addr)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("listen on netstack: %w", err)
|
|
}
|
|
return ln, fmt.Sprintf("netstack %s", addr), nil
|
|
}
|
|
|
|
var lc net.ListenConfig
|
|
ln, err := lc.Listen(ctx, "tcp", net.TCPAddrFromAddrPort(addr).String())
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("listen: %w", err)
|
|
}
|
|
return ln, addr.String(), nil
|
|
}
|
|
|
|
func (s *Server) runJanitor(ctx context.Context, done chan struct{}) {
|
|
defer close(done)
|
|
|
|
ticker := time.NewTicker(janitorInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
s.recv.expireOverdue()
|
|
}
|
|
}
|
|
}
|
|
|
|
func listenerPort(ln net.Listener, requested uint16) uint16 {
|
|
if tcpAddr, ok := ln.Addr().(*net.TCPAddr); ok {
|
|
return uint16(tcpAddr.Port)
|
|
}
|
|
return requested
|
|
}
|