Add peer-to-peer file drop

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.
This commit is contained in:
Zoltán Papp
2026-08-16 22:04:38 +02:00
parent 14aab0fc6e
commit 73cffdb702
70 changed files with 11240 additions and 332 deletions

View File

@@ -40,6 +40,7 @@ import (
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
"github.com/netbirdio/netbird/client/internal/dnsfwd"
"github.com/netbirdio/netbird/client/internal/expose"
"github.com/netbirdio/netbird/client/internal/filedrop"
"github.com/netbirdio/netbird/client/internal/ingressgw"
"github.com/netbirdio/netbird/client/internal/lazyconn"
"github.com/netbirdio/netbird/client/internal/metrics"
@@ -181,6 +182,7 @@ type EngineServices struct {
UpdateManager *updater.Manager
ClientMetrics *metrics.ClientMetrics
MetricsCtx context.Context
FileDrop *filedrop.Manager
}
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -236,6 +238,10 @@ type Engine struct {
sshServer sshServer
fileDrop *filedrop.Manager
fileDropRunning bool
fileDropPort uint16
statusRecorder *peer.Status
firewall firewallManager.Manager
@@ -350,6 +356,7 @@ func NewEngine(
metricsCtx: services.MetricsCtx,
updateManager: services.UpdateManager,
syncStoreDir: config.StateDir,
fileDrop: services.FileDrop,
}
// sessionWatcher keeps the SubscribeStatus consumers in sync with the
// session expiry deadline. Deadline-change ticks come for free via
@@ -415,6 +422,8 @@ func (e *Engine) stopLocked() {
log.Warnf("failed to stop SSH server: %v", err)
}
e.stopFileDrop()
e.cleanupSSHConfig()
if e.ingressGatewayMgr != nil {
@@ -1293,6 +1302,8 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
}
}
e.startFileDrop()
state := e.statusRecorder.GetLocalPeerState()
state.IP = e.wgInterface.Address().String()
state.IPv6 = e.wgInterface.Address().IPv6String()
@@ -1960,6 +1971,8 @@ func (e *Engine) receiveSignalEvents() error {
return err
}
e.recordFiledropPort(msg.Key, msg.GetBody().GetFiledropPort())
log.Debugf("receiveMSG: took %s to get lock for peer %s with session id %s", gotLock, msg.Key, offerAnswer.SessionID)
if msg.Body.Type == sProto.Body_OFFER {