diff --git a/client/internal/engine_filedrop.go b/client/internal/engine_filedrop.go index 42ba8c818..395fd8790 100644 --- a/client/internal/engine_filedrop.go +++ b/client/internal/engine_filedrop.go @@ -43,7 +43,7 @@ func (e *Engine) startFileDrop() { resolver := filedropResolver{status: e.statusRecorder} netstackNet := e.wgInterface.GetNet() - if err := e.fileDrop.StartReceiver(e.ctx, addr, netstackNet, resolver); err != nil { + 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 } diff --git a/client/internal/engine_filedrop_dial.go b/client/internal/engine_filedrop_dial.go index 3ace5d214..175440b23 100644 --- a/client/internal/engine_filedrop_dial.go +++ b/client/internal/engine_filedrop_dial.go @@ -12,3 +12,7 @@ func fileDropOSDial(WGIface) filedrop.DialFunc { dialer := &net.Dialer{} return dialer.DialContext } + +func fileDropListenControl(WGIface) filedrop.ListenControl { + return nil +} diff --git a/client/internal/engine_filedrop_dial_ios.go b/client/internal/engine_filedrop_dial_ios.go index 367fa5473..8ad925c1d 100644 --- a/client/internal/engine_filedrop_dial_ios.go +++ b/client/internal/engine_filedrop_dial_ios.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/netip" + "strings" "syscall" "golang.org/x/sys/unix" @@ -14,6 +15,31 @@ import ( "github.com/netbirdio/netbird/client/internal/filedrop" ) +// fileDropListenControl scopes the receiver's listeners to the tunnel +// interface, so replies on accepted connections leave through the tunnel +// instead of following the Network Extension's own-traffic bypass. +func fileDropListenControl(wgIface WGIface) filedrop.ListenControl { + return func(network, _ string, c syscall.RawConn) error { + osIface, err := net.InterfaceByName(wgIface.Name()) + if err != nil { + return fmt.Errorf("lookup interface %q: %w", wgIface.Name(), err) + } + + proto, opt := unix.IPPROTO_IP, unix.IP_BOUND_IF + if strings.HasSuffix(network, "6") { + proto, opt = unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF + } + + var operr error + if err := c.Control(func(s uintptr) { + operr = unix.SetsockoptInt(int(s), proto, opt, osIface.Index) + }); err != nil { + return err + } + return operr + } +} + // fileDropOSDial scopes the dial to the tunnel interface, since a Network // Extension's own unscoped sockets bypass its tunnel and leave on the // physical interface. diff --git a/client/internal/filedrop/manager.go b/client/internal/filedrop/manager.go index dae7dc79a..eec8f6a55 100644 --- a/client/internal/filedrop/manager.go +++ b/client/internal/filedrop/manager.go @@ -147,8 +147,9 @@ func (m *Manager) SetDestinationDir(dir string) error { return m.policy.SetDestinationDir(dir) } -// StartReceiver binds the receiving server on addr. -func (m *Manager) StartReceiver(ctx context.Context, addr netip.AddrPort, netstackNet *netstack.Net, resolver PeerResolver) error { +// StartReceiver binds the receiving server on addr. A non-nil control is +// applied to host listeners before bind. +func (m *Manager) StartReceiver(ctx context.Context, addr netip.AddrPort, netstackNet *netstack.Net, resolver PeerResolver, control ListenControl) error { m.mu.Lock() if m.server != nil { m.mu.Unlock() @@ -170,6 +171,9 @@ func (m *Manager) StartReceiver(ctx context.Context, addr netip.AddrPort, netsta if netstackNet != nil { server.SetNetstackNet(netstackNet) } + if control != nil { + server.SetListenControl(control) + } if err := server.Start(ctx, addr); err != nil { return fmt.Errorf("start receiver: %w", err) diff --git a/client/internal/filedrop/server.go b/client/internal/filedrop/server.go index 70a04d539..6bf43d792 100644 --- a/client/internal/filedrop/server.go +++ b/client/internal/filedrop/server.go @@ -8,6 +8,7 @@ import ( "net/http" "net/netip" "sync" + "syscall" "time" log "github.com/sirupsen/logrus" @@ -48,6 +49,10 @@ type ServerConfig struct { SpoolMaxAge time.Duration } +// ListenControl is a raw-socket hook applied to host listeners before bind, +// mirroring net.ListenConfig.Control. +type ListenControl func(network, address string, c syscall.RawConn) error + // 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 { @@ -56,6 +61,7 @@ type Server struct { listener net.Listener extraListeners []net.Listener netstackNet *netstack.Net + listenControl ListenControl recv *receiver boundPort uint16 @@ -90,6 +96,13 @@ func NewServer(cfg ServerConfig) (*Server, error) { return &Server{recv: newReceiver(cfg, spool, maxAge)}, nil } +// SetListenControl installs a raw-socket hook applied to host listeners. +func (s *Server) SetListenControl(control ListenControl) { + s.mu.Lock() + defer s.mu.Unlock() + s.listenControl = control +} + // SetNetstackNet routes listeners through the gVisor netstack instead of host sockets. func (s *Server) SetNetstackNet(n *netstack.Net) { s.mu.Lock() @@ -244,7 +257,7 @@ func (s *Server) createListener(ctx context.Context, addr netip.AddrPort) (net.L return ln, fmt.Sprintf("netstack %s", addr), nil } - var lc net.ListenConfig + lc := net.ListenConfig{Control: s.listenControl} ln, err := lc.Listen(ctx, "tcp", net.TCPAddrFromAddrPort(addr).String()) if err != nil { return nil, "", fmt.Errorf("listen: %w", err)