From 898539381c23dcf77625cd340d84232a1b4419a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Thu, 27 Aug 2026 16:10:25 +0200 Subject: [PATCH] [client] Scope the iOS file drop dial to the tunnel interface A Network Extension's own unscoped sockets bypass its tunnel and leave on the physical interface, so the file drop dial to a peer address died on the local network. Bind the socket to the overlay address and the tunnel interface index, the same way the iOS DNS upstream client dials. Other platforms keep the plain dialer: Android and the desktops route the client's own traffic into the tunnel already. --- client/internal/engine_filedrop_dial.go | 14 +++++ client/internal/engine_filedrop_dial_ios.go | 57 +++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 client/internal/engine_filedrop_dial.go create mode 100644 client/internal/engine_filedrop_dial_ios.go diff --git a/client/internal/engine_filedrop_dial.go b/client/internal/engine_filedrop_dial.go new file mode 100644 index 000000000..3ace5d214 --- /dev/null +++ b/client/internal/engine_filedrop_dial.go @@ -0,0 +1,14 @@ +//go:build !ios + +package internal + +import ( + "net" + + "github.com/netbirdio/netbird/client/internal/filedrop" +) + +func fileDropOSDial(WGIface) filedrop.DialFunc { + dialer := &net.Dialer{} + return dialer.DialContext +} diff --git a/client/internal/engine_filedrop_dial_ios.go b/client/internal/engine_filedrop_dial_ios.go new file mode 100644 index 000000000..367fa5473 --- /dev/null +++ b/client/internal/engine_filedrop_dial_ios.go @@ -0,0 +1,57 @@ +//go:build ios + +package internal + +import ( + "context" + "fmt" + "net" + "net/netip" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/netbirdio/netbird/client/internal/filedrop" +) + +// 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. +func fileDropOSDial(wgIface WGIface) filedrop.DialFunc { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + addrPort, err := netip.ParseAddrPort(addr) + if err != nil { + return nil, err + } + + osIface, err := net.InterfaceByName(wgIface.Name()) + if err != nil { + return nil, fmt.Errorf("lookup interface %q: %w", wgIface.Name(), err) + } + + wgAddr := wgIface.Address() + bindIP := wgAddr.IP + proto, opt := unix.IPPROTO_IP, unix.IP_BOUND_IF + if addrPort.Addr().Is6() { + if !wgAddr.HasIPv6() { + return nil, fmt.Errorf("no IPv6 address on %s", wgIface.Name()) + } + bindIP = wgAddr.IPv6 + proto, opt = unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF + } + + dialer := &net.Dialer{ + LocalAddr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(bindIP, 0)), + Control: func(_, _ string, c syscall.RawConn) error { + 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 + }, + } + return dialer.DialContext(ctx, network, addr) + } +}