Files
netbird/client/internal/engine_filedrop.go
T
Zoltán Papp 2d03a2e7b0 [client] Fix lint findings on the file drop overlay wait
gosec G101 matched the identifier fileDropWatchName: its default pattern
includes a bare "pw", which hits case-insensitively inside "fileDropWatch",
and the constant's value then cleared the entropy threshold. Nothing here is
credential-shaped, so rename rather than suppress. Any fileDropW... spelling
keeps the match, hence fileDropReceiverLabel.

overlayWait and the non-iOS overlayWaiter are read only by the iOS overlay
address wait, and the linter analyses the default build only. Suppress with a
reason; the field stays in engine.go as part of the lifecycle.
2026-09-08 20:33:04 +02:00

255 lines
7.1 KiB
Go

package internal
import (
"context"
"net"
"net/netip"
"os"
"strconv"
log "github.com/sirupsen/logrus"
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/filedrop"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
"github.com/netbirdio/netbird/client/internal/peer"
)
const fileDropReceiverLabel = "the file drop receiver"
type filedropResolver struct {
status *peer.Status
}
// ResolvePeer implements filedrop.PeerResolver.
func (r filedropResolver) ResolvePeer(addr netip.Addr) (filedrop.PeerKey, string, bool) {
state, ok := r.status.PeerStateByIP(addr.String())
if !ok {
return "", "", false
}
return filedrop.PeerKey(state.PubKey), state.FQDN, true
}
func (e *Engine) startFileDrop() {
if e.fileDrop == nil || e.fileDropRunning || e.wgInterface == nil {
return
}
e.setFileDropTunnel()
e.fileDrop.SetReceivingChangeHandler(e.onFileDropPolicyChange)
if e.config.BlockInbound {
log.Info("file drop receiver is disabled because inbound connections are blocked")
return
}
if !e.fileDrop.ReceivingEnabled() {
log.Info("file drop receiver is not started because receiving is turned off")
return
}
wgAddr := e.wgInterface.Address()
if !e.overlayAddrReady(wgAddr.IP) {
log.Infof("file drop receiver waits for the overlay address %s", wgAddr.IP)
e.armOverlayWatch(fileDropReceiverLabel, e.restartFileDrop)
return
}
addr := netip.AddrPortFrom(wgAddr.IP, fileDropListenPort())
resolver := filedropResolver{status: e.statusRecorder}
netstackNet := e.wgInterface.GetNet()
if err := e.fileDrop.StartReceiver(e.ctx, addr, netstackNet, resolver, fileDropListenControl(e.wgInterface)); err != nil {
log.Errorf("failed to start file drop receiver: %v", err)
return
}
bound := e.fileDrop.ReceiverPort()
if bound == 0 {
bound = addr.Port()
}
e.fileDropPort = bound
if v6 := wgAddr.IPv6; v6.IsValid() {
if err := e.fileDrop.AddReceiverListener(e.ctx, netip.AddrPortFrom(v6, bound)); err != nil {
log.Warnf("failed to add IPv6 file drop listener: %v", err)
e.armOverlayWatch(fileDropReceiverLabel, e.restartFileDrop)
}
}
if netstackNet != nil {
if registrar, ok := e.firewall.(interface {
RegisterNetstackService(protocol nftypes.Protocol, port uint16)
}); ok {
registrar.RegisterNetstackService(nftypes.TCP, bound)
}
}
e.setupFileDropPortRedirection(bound)
e.fileDropRunning = true
}
// setupFileDropPortRedirection keeps the tunnel-side port fixed when the receiver
// could not bind it, so senders always reach the well-known port.
func (e *Engine) setupFileDropPortRedirection(bound uint16) {
if e.firewall == nil || bound == filedrop.Port {
return
}
for _, addr := range e.fileDropLocalAddrs() {
if err := e.firewall.AddInboundDNAT(addr, firewallManager.ProtocolTCP, filedrop.Port, bound); err != nil {
log.Warnf("failed to add file drop port redirection on %s: %v", addr, err)
continue
}
log.Infof("file drop port redirection enabled: %s:%d -> %s:%d", addr, filedrop.Port, addr, bound)
}
}
func (e *Engine) removeFileDropPortRedirection(bound uint16) {
if e.firewall == nil || bound == 0 || bound == filedrop.Port {
return
}
for _, addr := range e.fileDropLocalAddrs() {
if err := e.firewall.RemoveInboundDNAT(addr, firewallManager.ProtocolTCP, filedrop.Port, bound); err != nil {
log.Warnf("failed to remove file drop port redirection on %s: %v", addr, err)
continue
}
log.Debugf("file drop port redirection removed: %s:%d -> %s:%d", addr, filedrop.Port, addr, bound)
}
}
func (e *Engine) fileDropLocalAddrs() []netip.Addr {
if e.wgInterface == nil {
return nil
}
wgAddr := e.wgInterface.Address()
var addrs []netip.Addr
if wgAddr.IP.IsValid() {
addrs = append(addrs, wgAddr.IP)
}
if wgAddr.IPv6.IsValid() {
addrs = append(addrs, wgAddr.IPv6)
}
return addrs
}
func (e *Engine) setFileDropTunnel() {
var dial filedrop.DialFunc
if netstackNet := e.wgInterface.GetNet(); netstackNet != nil {
dial = func(ctx context.Context, _, addr string) (net.Conn, error) {
addrPort, err := netip.ParseAddrPort(addr)
if err != nil {
return nil, err
}
return netstackNet.DialContextTCPAddrPort(ctx, addrPort)
}
} else {
dial = fileDropOSDial(e.wgInterface)
}
e.fileDrop.SetTunnel(dial, e.statusRecorder.GetLocalPeerState().FQDN)
}
// restartFileDrop gives the receiver listeners on the interface as it is now.
// The listeners are bound to the overlay address of the interface being swapped
// out and do not survive it: Android renews the tun on every route change, which
// leaves the IPv4 listener dead with accept4: invalid argument. A receiver that
// is not running is started rather than skipped, since the reason it is down may
// be the very bind this rebind can now make. See Engine.rebindOverlayListeners.
func (e *Engine) restartFileDrop() error {
if e.fileDrop == nil || e.wgInterface == nil {
return nil
}
if !e.fileDropRunning {
e.startFileDrop()
return nil
}
e.stopFileDrop()
e.startFileDrop()
return nil
}
// onFileDropPolicyChange binds or unbinds the receiver after the profile turned
// receiving on or off. The work is handed to a goroutine because it needs
// syncMsgMux, which the caller (a settings RPC) must not wait on and which the
// engine may itself hold while calling into the manager.
func (e *Engine) onFileDropPolicyChange() {
if e.ctx.Err() != nil {
return
}
e.shutdownWg.Add(1)
go func() {
defer e.shutdownWg.Done()
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
if e.fileDrop == nil || e.ctx.Err() != nil {
return
}
if e.fileDrop.ReceivingEnabled() {
e.startFileDrop()
return
}
if e.fileDropRunning {
e.unbindFileDropReceiver()
if err := e.fileDrop.DisableReceiving(); err != nil {
log.Warnf("failed to stop file drop receiver: %v", err)
}
e.fileDropRunning = false
e.fileDropPort = 0
}
}()
}
// unbindFileDropReceiver releases what the bind claimed outside the receiver
// itself. The caller must hold syncMsgMux.
func (e *Engine) unbindFileDropReceiver() {
if netstackNet := e.wgInterface.GetNet(); netstackNet != nil {
if registrar, ok := e.firewall.(interface {
UnregisterNetstackService(protocol nftypes.Protocol, port uint16)
}); ok {
registrar.UnregisterNetstackService(nftypes.TCP, e.fileDropPort)
}
}
e.removeFileDropPortRedirection(e.fileDropPort)
}
func (e *Engine) stopFileDrop() {
if e.fileDrop == nil {
return
}
e.fileDrop.SetReceivingChangeHandler(nil)
e.fileDrop.ClearTunnel()
if e.fileDropRunning {
e.unbindFileDropReceiver()
}
if err := e.fileDrop.StopReceiver(); err != nil {
log.Warnf("failed to stop file drop receiver: %v", err)
}
e.fileDropRunning = false
e.fileDropPort = 0
}
// fileDropListenPort is the port the receiver tries to bind locally; the
// tunnel-side port stays filedrop.Port whatever this resolves to.
func fileDropListenPort() uint16 {
raw := os.Getenv(filedrop.EnvPort)
if raw == "" {
return filedrop.Port
}
port, err := strconv.ParseUint(raw, 10, 16)
if err != nil {
log.Warnf("invalid %s value %q, using %d", filedrop.EnvPort, raw, filedrop.Port)
return filedrop.Port
}
return uint16(port)
}