diff --git a/client/iface/bind/ice_bind.go b/client/iface/bind/ice_bind.go index 156450c61..bf79ecd79 100644 --- a/client/iface/bind/ice_bind.go +++ b/client/iface/bind/ice_bind.go @@ -41,6 +41,7 @@ type ICEBind struct { *wgConn.StdNetBind transportNet transport.Net + filterFn udpmux.FilterFn address wgaddr.Address mtu uint16 @@ -60,11 +61,12 @@ type ICEBind struct { ipv6Conn *net.UDPConn } -func NewICEBind(transportNet transport.Net, address wgaddr.Address, mtu uint16) *ICEBind { +func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wgaddr.Address, mtu uint16) *ICEBind { b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind) ib := &ICEBind{ StdNetBind: b, transportNet: transportNet, + filterFn: filterFn, address: address, mtu: mtu, endpoints: make(map[netip.Addr]net.Conn), @@ -263,6 +265,7 @@ func (s *ICEBind) createOrUpdateMux() { udpmux.UniversalUDPMuxParams{ UDPConn: muxConn, Net: s.transportNet, + FilterFn: s.filterFn, WGAddress: s.address, MTU: s.mtu, }, diff --git a/client/iface/bind/ice_bind_test.go b/client/iface/bind/ice_bind_test.go index 0b8db7640..f49e68508 100644 --- a/client/iface/bind/ice_bind_test.go +++ b/client/iface/bind/ice_bind_test.go @@ -289,7 +289,7 @@ func setupICEBind(t *testing.T) *ICEBind { IP: netip.MustParseAddr("100.64.0.1"), Network: netip.MustParsePrefix("100.64.0.0/10"), } - return NewICEBind(transportNet, address, 1280) + return NewICEBind(transportNet, nil, address, 1280) } func createDualStackConns(t *testing.T) (*net.UDPConn, *net.UDPConn) { diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go index 3c429fb96..25c4148a6 100644 --- a/client/iface/device/device_kernel_unix.go +++ b/client/iface/device/device_kernel_unix.go @@ -32,6 +32,8 @@ type TunKernelDevice struct { link *wgLink udpMuxConn net.PacketConn udpMux *udpmux.UniversalUDPMuxDefault + + filterFn udpmux.FilterFn } func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice { @@ -102,6 +104,7 @@ func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) { bindParams := udpmux.UniversalUDPMuxParams{ UDPConn: nbnet.WrapPacketConn(rawSock), Net: t.transportNet, + FilterFn: t.filterFn, WGAddress: t.address, MTU: t.mtu, } diff --git a/client/iface/iface.go b/client/iface/iface.go index 247f421a2..78c5080e7 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -63,6 +63,7 @@ type WGIFaceOpts struct { MTU uint16 MobileArgs *device.MobileIFaceArguments TransportNet transport.Net + FilterFn udpmux.FilterFn DisableDNS bool } diff --git a/client/iface/iface_new.go b/client/iface/iface_new.go index 96a0e670f..28f350e3f 100644 --- a/client/iface/iface_new.go +++ b/client/iface/iface_new.go @@ -11,7 +11,7 @@ import ( // NewWGIFace Creates a new WireGuard interface instance func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) { - iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU) + iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU) var tun WGTunDevice if netstack.IsEnabled() { diff --git a/client/iface/iface_new_android.go b/client/iface/iface_new_android.go index ce8b4da23..e28dcc0de 100644 --- a/client/iface/iface_new_android.go +++ b/client/iface/iface_new_android.go @@ -9,7 +9,7 @@ import ( // NewWGIFace Creates a new WireGuard interface instance func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) { - iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU) + iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU) if netstack.IsEnabled() { wgIFace := &WGIface{ diff --git a/client/iface/iface_new_ios.go b/client/iface/iface_new_ios.go index cedd55ce2..41e0022b2 100644 --- a/client/iface/iface_new_ios.go +++ b/client/iface/iface_new_ios.go @@ -10,7 +10,7 @@ import ( // NewWGIFace Creates a new WireGuard interface instance func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) { - iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU) + iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU) wgIFace := &WGIface{ tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunFd), diff --git a/client/iface/iface_new_linux.go b/client/iface/iface_new_linux.go index 2465130e6..65ce67e88 100644 --- a/client/iface/iface_new_linux.go +++ b/client/iface/iface_new_linux.go @@ -14,7 +14,7 @@ import ( // NewWGIFace Creates a new WireGuard interface instance func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) { if netstack.IsEnabled() { - iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU) + iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU) return &WGIface{ tun: device.NewNetstackDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr()), userspaceBind: true, @@ -30,7 +30,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) { } if device.ModuleTunIsLoaded() { - iceBind := bind.NewICEBind(opts.TransportNet, opts.Address, opts.MTU) + iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, opts.Address, opts.MTU) return &WGIface{ tun: device.NewTunDevice(opts.IFaceName, opts.Address, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind), userspaceBind: true, diff --git a/client/iface/udpmux/universal.go b/client/iface/udpmux/universal.go index 77e1b1b35..89a7eefb9 100644 --- a/client/iface/udpmux/universal.go +++ b/client/iface/udpmux/universal.go @@ -8,6 +8,8 @@ import ( "context" "fmt" "net" + "net/netip" + "sync" "time" log "github.com/sirupsen/logrus" @@ -20,6 +22,10 @@ import ( "github.com/netbirdio/netbird/client/iface/wgaddr" ) +// FilterFn is a function that filters out candidates based on the address. +// If it returns true, the address is to be filtered. It also returns the prefix of matching route. +type FilterFn func(address netip.Addr) (bool, netip.Prefix, error) + // UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn // It then passes packets to the UDPMux that does the actual connection muxing. type UniversalUDPMuxDefault struct { @@ -37,6 +43,7 @@ type UniversalUDPMuxParams struct { UDPConn net.PacketConn XORMappedAddrCacheTTL time.Duration Net transport.Net + FilterFn FilterFn WGAddress wgaddr.Address MTU uint16 } @@ -61,6 +68,7 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef PacketConn: params.UDPConn, mux: m, logger: params.Logger, + filterFn: params.FilterFn, address: params.WGAddress, } @@ -107,12 +115,15 @@ func (m *UniversalUDPMuxDefault) ReadFromConn(ctx context.Context) { } } -// UDPConn is a wrapper around UDPMux conn that overrides WriteTo to drop packets destined for the overlay subnet. +// UDPConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets type UDPConn struct { net.PacketConn - mux *UniversalUDPMuxDefault - logger logging.LeveledLogger - address wgaddr.Address + mux *UniversalUDPMuxDefault + logger logging.LeveledLogger + filterFn FilterFn + // TODO: reset cache on route changes + addrCache sync.Map + address wgaddr.Address } // GetPacketConn returns the underlying PacketConn @@ -121,18 +132,67 @@ func (u *UDPConn) GetPacketConn() net.PacketConn { } func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { - udpAddr, ok := addr.(*net.UDPAddr) - if !ok { + if u.filterFn == nil { return u.PacketConn.WriteTo(b, addr) } - dst := udpAddr.AddrPort().Addr().Unmap() - if (u.address.Network.IsValid() && u.address.Network.Contains(dst)) || (u.address.IPv6Net.IsValid() && u.address.IPv6Net.Contains(dst)) { - log.Warnf("address %s is part of the NetBird network %s, refusing to write", addr, u.address) - return 0, fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address) + + if isRouted, found := u.addrCache.Load(addr.String()); found { + return u.handleCachedAddress(isRouted.(bool), b, addr) + } + + return u.handleUncachedAddress(b, addr) +} + +func (u *UDPConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) { + if isRouted { + return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr) } return u.PacketConn.WriteTo(b, addr) } +func (u *UDPConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) { + if err := u.performFilterCheck(addr); err != nil { + return 0, err + } + return u.PacketConn.WriteTo(b, addr) +} + +func (u *UDPConn) performFilterCheck(addr net.Addr) error { + host, err := getHostFromAddr(addr) + if err != nil { + log.Errorf("Failed to get host from address %s: %v", addr, err) + return nil + } + + a, err := netip.ParseAddr(host) + if err != nil { + log.Errorf("Failed to parse address %s: %v", addr, err) + return nil + } + + if u.address.Network.Contains(a) { + log.Warnf("address %s is part of the NetBird network %s, refusing to write", addr, u.address) + return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address) + } + + if isRouted, prefix, err := u.filterFn(a); err != nil { + log.Errorf("Failed to check if address %s is routed: %v", addr, err) + } else { + u.addrCache.Store(addr.String(), isRouted) + if isRouted { + // Extra log, as the error only shows up with ICE logging enabled + log.Infof("address %s is part of routed network %s, refusing to write", addr, prefix) + return fmt.Errorf("address %s is part of routed network %s, refusing to write", addr, prefix) + } + } + return nil +} + +func getHostFromAddr(addr net.Addr) (string, error) { + host, _, err := net.SplitHostPort(addr.String()) + return host, err +} + // GetSharedConn returns the shared udp conn func (m *UniversalUDPMuxDefault) GetSharedConn() net.PacketConn { return m.params.UDPConn @@ -165,13 +225,6 @@ func (m *UniversalUDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.A return nil } - src := udpAddr.AddrPort().Addr().Unmap() - wg := m.params.WGAddress - if (wg.Network.IsValid() && wg.Network.Contains(src)) || (wg.IPv6Net.IsValid() && wg.IPv6Net.Contains(src)) { - log.Debugf("dropping STUN message from overlay source %s", udpAddr) - return nil - } - if m.isXORMappedResponse(msg, udpAddr.String()) { err := m.handleXORMappedResponse(udpAddr, msg) if err != nil { diff --git a/client/iface/wgproxy/proxy_linux_test.go b/client/iface/wgproxy/proxy_linux_test.go index 7f7abcb4a..dd24d1cdc 100644 --- a/client/iface/wgproxy/proxy_linux_test.go +++ b/client/iface/wgproxy/proxy_linux_test.go @@ -66,7 +66,7 @@ func seedProxyForProxyCloseByRemoteConn() ([]proxyInstance, error) { if err != nil { return nil, err } - iceBind := bind.NewICEBind(nil, wgAddress, 1280) + iceBind := bind.NewICEBind(nil, nil, wgAddress, 1280) endpointAddress := &net.UDPAddr{ IP: net.IPv4(10, 0, 0, 1), Port: 1234, diff --git a/client/iface/wgproxy/proxy_seed_test.go b/client/iface/wgproxy/proxy_seed_test.go index 9278029a5..ad375ccde 100644 --- a/client/iface/wgproxy/proxy_seed_test.go +++ b/client/iface/wgproxy/proxy_seed_test.go @@ -22,7 +22,7 @@ func seedProxyForProxyCloseByRemoteConn() ([]proxyInstance, error) { if err != nil { return nil, err } - iceBind := bind.NewICEBind(nil, wgAddress, 1280) + iceBind := bind.NewICEBind(nil, nil, wgAddress, 1280) endpointAddress := &net.UDPAddr{ IP: net.IPv4(10, 0, 0, 1), Port: 1234, diff --git a/client/internal/engine.go b/client/internal/engine.go index fd9525617..76829381f 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -54,6 +54,7 @@ import ( "github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/client/internal/rosenpass" "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/syncstore" "github.com/netbirdio/netbird/client/internal/updater" @@ -1994,6 +1995,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) { WGPrivKey: e.config.WgPrivateKey.String(), MTU: e.config.MTU, TransportNet: transportNet, + FilterFn: e.addrViaRoutes, DisableDNS: e.config.DisableDNS, } @@ -2248,6 +2250,21 @@ func (e *Engine) startNetworkMonitor() { }() } +func (e *Engine) addrViaRoutes(addr netip.Addr) (bool, netip.Prefix, error) { + var vpnRoutes []netip.Prefix + for _, routes := range e.routeManager.GetClientRoutes() { + if len(routes) > 0 && routes[0] != nil { + vpnRoutes = append(vpnRoutes, routes[0].Network) + } + } + + if isVpn, prefix := systemops.IsAddrRouted(addr, vpnRoutes); isVpn { + return true, prefix, nil + } + + return false, netip.Prefix{}, nil +} + func (e *Engine) stopDNSServer() { if e.dnsServer == nil { return diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go index 34f0ab93d..b8ad229c1 100644 --- a/client/internal/peer/worker_ice.go +++ b/client/internal/peer/worker_ice.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "strconv" "sync" "time" @@ -164,6 +165,10 @@ func (w *WorkerICE) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HA return } + if candidateViaRoutes(candidate, haRoutes) { + return + } + if err := w.agent.AddRemoteCandidate(candidate); err != nil { w.log.Errorf("error while handling remote candidate") return @@ -584,6 +589,34 @@ func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive return ec, nil } +func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool { + addr, err := netip.ParseAddr(candidate.Address()) + if err != nil { + log.Errorf("Failed to parse IP address %s: %v", candidate.Address(), err) + return false + } + + var routePrefixes []netip.Prefix + for _, routes := range clientRoutes { + if len(routes) > 0 && routes[0] != nil { + routePrefixes = append(routePrefixes, routes[0].Network) + } + } + + for _, prefix := range routePrefixes { + // default route is handled by route exclusion / ip rules + if prefix.Bits() == 0 { + continue + } + + if prefix.Contains(addr) { + log.Debugf("Ignoring candidate [%s], its address is part of routed network %s", candidate.String(), prefix) + return true + } + } + return false +} + func isRelayCandidate(candidate ice.Candidate) bool { return candidate.Type() == ice.CandidateTypeRelay } diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go index bb9ac494d..2b96c14dc 100644 --- a/client/internal/routemanager/systemops/systemops_generic.go +++ b/client/internal/routemanager/systemops/systemops_generic.go @@ -121,12 +121,9 @@ func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf wgIface, init return Nexthop{}, vars.ErrRouteNotAllowed } - // BSDs blackhole a /32 added inside a directly-connected subnet; Linux/Windows need it to beat the wt0 route. - switch runtime.GOOS { - case "darwin", "freebsd", "netbsd", "openbsd", "dragonfly": - if isLocal, subnet := r.isPrefixInLocalSubnets(prefix); isLocal { - return Nexthop{}, fmt.Errorf("prefix %s is part of local subnet %s: %w", prefix, subnet, vars.ErrRouteNotAllowed) - } + // Check if the prefix is part of any local subnets + if isLocal, subnet := r.isPrefixInLocalSubnets(prefix); isLocal { + return Nexthop{}, fmt.Errorf("prefix %s is part of local subnet %s: %w", prefix, subnet, vars.ErrRouteNotAllowed) } // Determine the exit interface and next hop for the prefix, so we can add a specific route