[GH-ISSUE #5636] ICE/P2P fails on Ubiquiti UXG-Max: legacy routing route-check can't find default route in policy routing tables #10882

Open
opened 2026-08-05 01:27:35 -04:00 by saavagebueno · 1 comment
Owner

Originally created by @thvevirtue on GitHub (Mar 20, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/5636

ICE/P2P fails on Ubiquiti UXG-Max: legacy routing route-check can't find default route in policy routing tables

NetBird version
0.66.4

Environment

  • Device: Ubiquiti UXG-Max
  • Kernel: 5.4.213-ui-ipq5322 (ARM64, Qualcomm IPQ5322)
  • OS: Debian GNU/Linux 11 (bullseye)
  • iptables tables available: filter, nat, mangle, raw
  • Self-hosted management at management.example.com

Describe the problem

On the Ubiquiti UXG-Max, all peer connections remain Relayed — P2P/ICE never establishes. Management, Signal, and Relay connect successfully with NB_USE_LEGACY_ROUTING=true.

The root cause is that the UXG-Max uses policy-based routing where the default gateway lives in a custom routing table (201.eth4), not in the main table:

ip route show table main

192.168.1.0/24 dev br0 proto kernel scope link src 192.168.1.1
... (only directly connected subnets, NO default route)

ip route show table 201.eth4

default via dev eth4 proto dhcp metric 200

ip rule show

0: from all lookup local
32000: from all lookup main
32501: from all fwmark 0x1a0000/0x7e0000 lookup 201.eth4
32766: from all lookup 201.eth4
32767: from all lookup default

Normal TCP traffic (gRPC to management/signal, WS to relay) works because the kernel resolves routes through the policy routing rules. But NetBird's ICE packet-sending code in systemops_generic.go performs its own route lookup that only checks the main table. Since there's no default route in main, every STUN and ICE connectivity-check packet fails with:

ice WARNING: Failed get server reflexive address udp4 stun:stun.example.com:3479:
failed to send STUN packet: got an error while checking route, err: Route referenced unknown interface

ice INFO: Failed to send packet: got an error while checking route, err: Route referenced unknown interface

This is repeated continuously. Local ICE candidates are gathered successfully, and remote candidates are received — but no packets can ever be sent, so ICE always times out and falls back to relay.

Logs (trimmed)

2026-03-20T04:04:41.124Z INFO Using legacy routing setup
2026-03-20T04:04:41.107Z WARN failed to initialize ebpf proxy, fallback to user space proxy: bpf_link not supported (requires >= v5.7)

ice DEBUG: Started agent: isControlling? true
ice INFO: Setting new connection state: Checking
DEBG worker_ice.go: discovered local candidate udp4 host <local_ip>:51820

ice WARNING: Failed get server reflexive address udp4 stun:stun.example.com:3479:
failed to send STUN packet: got an error while checking route, err: Route referenced unknown interface

DEBG systemops_generic.go:348: Failed to get route for <peer_ip>: Route referenced unknown interface
ice INFO: Failed to send packet: got an error while checking route, err: Route referenced unknown interface

netbird status -d

Peers detail:
peer-1.example.internal:
Connection type: Relayed
ICE candidate (Local/Remote): -/-
ICE candidate endpoints (Local/Remote): -/-
Relay server address: rels://relay.example.com:443

peer-2.example.internal:
Connection type: Relayed
...

(all peers: Relayed, ICE -/-)

OS: linux/arm64
Daemon version: 0.66.4
Management: Connected
Signal: Connected
Relays:
[stun:stun.example.com:3479] is Available
[rels://relay.example.com:443] is Available
Peers count: 5/5 Connected

System info

uname -a

Linux host 5.4.213-ui-ipq5322 #... aarch64 GNU/Linux

ip route show table main

192.168.1.0/24 dev br0 proto kernel scope link src 192.168.1.1


(NO default route)

ip route show table 201.eth4

default via dev eth4 proto dhcp metric 200

ip rule show

0: from all lookup local
32000: from all lookup main
32500: from all iif eth4 goto 32502
32501: from all fwmark 0x1a0000/0x7e0000 lookup 201.eth4
32502: from all lookup 201.eth4
32765: from all fwmark 0x10000/0x10000 lookup 251.blackhole
32766: from all lookup 201.eth4
32767: from all lookup default

Root cause analysis

systemops_generic.go performs a route lookup that only consults the main routing table.

On UXG-Max, the default route is not in main — it exists in a policy routing table (201.eth4). The kernel correctly routes packets via ip rule, but NetBird’s route check does not.

This results in:

  • gRPC / management: works
  • Relay: works
  • ICE/STUN: fails

Expected behavior

ICE packets should follow the kernel’s routing decisions (including policy routing), not just the main table.

Suggested fix

Option 1 (preferred):
Remove or make optional the application-level route check and rely on kernel routing.

Option 2:
Update route-check logic to evaluate all routing tables referenced by ip rule.

Option 3:
Add environment variable:
NB_ICE_SKIP_ROUTE_CHECK=true

Affected platforms

  • Ubiquiti UXG-Max
  • Ubiquiti UXG-Enterprise
  • UniFi OS gateway devices
  • Other enterprise routers

Related issues

Originally created by @thvevirtue on GitHub (Mar 20, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/5636 ICE/P2P fails on Ubiquiti UXG-Max: legacy routing route-check can't find default route in policy routing tables NetBird version 0.66.4 Environment - Device: Ubiquiti UXG-Max - Kernel: 5.4.213-ui-ipq5322 (ARM64, Qualcomm IPQ5322) - OS: Debian GNU/Linux 11 (bullseye) - iptables tables available: filter, nat, mangle, raw - Self-hosted management at management.example.com Describe the problem On the Ubiquiti UXG-Max, all peer connections remain Relayed — P2P/ICE never establishes. Management, Signal, and Relay connect successfully with NB_USE_LEGACY_ROUTING=true. The root cause is that the UXG-Max uses policy-based routing where the default gateway lives in a custom routing table (201.eth4), not in the main table: # ip route show table main 192.168.1.0/24 dev br0 proto kernel scope link src 192.168.1.1 ... (only directly connected subnets, NO default route) # ip route show table 201.eth4 default via <gateway> dev eth4 proto dhcp metric 200 # ip rule show 0: from all lookup local 32000: from all lookup main 32501: from all fwmark 0x1a0000/0x7e0000 lookup 201.eth4 32766: from all lookup 201.eth4 32767: from all lookup default Normal TCP traffic (gRPC to management/signal, WS to relay) works because the kernel resolves routes through the policy routing rules. But NetBird's ICE packet-sending code in systemops_generic.go performs its own route lookup that only checks the main table. Since there's no default route in main, every STUN and ICE connectivity-check packet fails with: ice WARNING: Failed get server reflexive address udp4 stun:stun.example.com:3479: failed to send STUN packet: got an error while checking route, err: Route referenced unknown interface ice INFO: Failed to send packet: got an error while checking route, err: Route referenced unknown interface This is repeated continuously. Local ICE candidates are gathered successfully, and remote candidates are received — but no packets can ever be sent, so ICE always times out and falls back to relay. Logs (trimmed) 2026-03-20T04:04:41.124Z INFO Using legacy routing setup 2026-03-20T04:04:41.107Z WARN failed to initialize ebpf proxy, fallback to user space proxy: bpf_link not supported (requires >= v5.7) ice DEBUG: Started agent: isControlling? true ice INFO: Setting new connection state: Checking DEBG worker_ice.go: discovered local candidate udp4 host <local_ip>:51820 ice WARNING: Failed get server reflexive address udp4 stun:stun.example.com:3479: failed to send STUN packet: got an error while checking route, err: Route referenced unknown interface DEBG systemops_generic.go:348: Failed to get route for <peer_ip>: Route referenced unknown interface ice INFO: Failed to send packet: got an error while checking route, err: Route referenced unknown interface netbird status -d Peers detail: peer-1.example.internal: Connection type: Relayed ICE candidate (Local/Remote): -/- ICE candidate endpoints (Local/Remote): -/- Relay server address: rels://relay.example.com:443 peer-2.example.internal: Connection type: Relayed ... (all peers: Relayed, ICE -/-) OS: linux/arm64 Daemon version: 0.66.4 Management: Connected Signal: Connected Relays: [stun:stun.example.com:3479] is Available [rels://relay.example.com:443] is Available Peers count: 5/5 Connected System info # uname -a Linux host 5.4.213-ui-ipq5322 #... aarch64 GNU/Linux # ip route show table main 192.168.1.0/24 dev br0 proto kernel scope link src 192.168.1.1 <other directly connected subnets> <NetBird wt0 subnet> (NO default route) # ip route show table 201.eth4 default via <gateway> dev eth4 proto dhcp metric 200 # ip rule show 0: from all lookup local 32000: from all lookup main 32500: from all iif eth4 goto 32502 32501: from all fwmark 0x1a0000/0x7e0000 lookup 201.eth4 32502: from all lookup 201.eth4 32765: from all fwmark 0x10000/0x10000 lookup 251.blackhole 32766: from all lookup 201.eth4 32767: from all lookup default Root cause analysis systemops_generic.go performs a route lookup that only consults the main routing table. On UXG-Max, the default route is not in main — it exists in a policy routing table (201.eth4). The kernel correctly routes packets via ip rule, but NetBird’s route check does not. This results in: - gRPC / management: works - Relay: works - ICE/STUN: fails Expected behavior ICE packets should follow the kernel’s routing decisions (including policy routing), not just the main table. Suggested fix Option 1 (preferred): Remove or make optional the application-level route check and rely on kernel routing. Option 2: Update route-check logic to evaluate all routing tables referenced by ip rule. Option 3: Add environment variable: NB_ICE_SKIP_ROUTE_CHECK=true Affected platforms - Ubiquiti UXG-Max - Ubiquiti UXG-Enterprise - UniFi OS gateway devices - Other enterprise routers Related issues - #5551 - #2530 - #2116
saavagebueno added the triage-needed label 2026-08-05 01:27:35 -04:00
Author
Owner

@thvevirtue commented on GitHub (Mar 23, 2026):

ICE/P2P fails on devices with policy-based routing (UniFi gateways, etc.) — go-netroute only queries main table

NetBird version
0.66.4

Environment
• Device: Ubiquiti UXG-Max
• Kernel: 5.4.213-ui-ipq5322 (ARM64, Qualcomm IPQ5322)
• OS: Debian GNU/Linux 11 (bullseye)
• iptables tables available: filter, nat, mangle, raw
• Self-hosted management server

Describe the problem

On the Ubiquiti UXG-Max (and likely all UniFi gateway devices), all peer connections remain Relayed — P2P/ICE never establishes. Management, Signal, and Relay connect successfully with NB_USE_LEGACY_ROUTING=true.

Root cause: The UXG-Max uses policy-based routing where the default gateway lives in a custom routing table (201.eth4), not in the main table:

ip route show table main

192.168.1.0/24 dev br0 proto kernel scope link src 192.168.1.1
(... only directly connected subnets, NO default route)

ip route show table 201.eth4

default via dev eth4 proto dhcp metric 200

ip rule show

0: from all lookup local
32000: from all lookup main
32501: from all fwmark 0x1a0000/0x7e0000 lookup 201.eth4
32766: from all lookup 201.eth4
32767: from all lookup default

NetBird uses go-netroute (github.com/libp2p/go-netroute, forked at github.com/netbirdio/go-netroute) for route lookups. This library calls syscall.NetlinkRIB(RTM_GETROUTE, AF_UNSPEC) which only dumps routes from the main table (254). Since there's no default route in main, all route lookups for external IPs fail.

This breaks two code paths:

  1. SharedSocket.WriteTo() (sharedsock/sock_linux.go:316) — calls s.router.Route() to determine source IP for raw UDP packets. Fails with "Route referenced unknown interface".
  2. GetNextHop() (client/internal/routemanager/systemops/systemops_generic.go:346) — called via write hooks when ICE sends packets. Same failure.

Result: Every STUN and ICE connectivity-check packet is dropped before reaching the kernel. Local ICE candidates are gathered successfully and remote candidates are received from peers, but no packets can ever be sent. ICE checking always times out and falls back to relay.

Normal TCP traffic (gRPC to management/signal, WS to relay) works fine because standard Go net.Dial lets the kernel handle routing through policy rules.

Logs (key lines)

INFO Using legacy routing setup
WARN failed to initialize ebpf proxy, fallback to user space proxy: bpf_link not supported (requires >= v5.7)

ICE agents start and gather local candidates successfully:

DEBG worker_ice.go: discovered local candidate udp4 host <local_ip>:51820

But STUN fails — route check can't find route in main table:

ice WARNING: Failed get server reflexive address udp4 stun::3479:
failed to send STUN packet: got an error while checking route, err: Route referenced unknown interface

All ICE connectivity checks fail the same way:

DEBG systemops_generic.go:348: Failed to get route for <peer_ip>: Route referenced unknown interface
ice INFO: Failed to send packet: got an error while checking route, err: Route referenced unknown interface
(repeated hundreds of times for every peer)

netbird status -d output

Peers detail:
peer1.example.vpn:
Connection type: Relayed
ICE candidate (Local/Remote): -/-
ICE candidate endpoints (Local/Remote): -/-
Relay server address: rels://:443

(all 5 peers: Relayed, ICE -/-)

Management: Connected
Signal: Connected
Relays: [stun::3479] is Available, [rels://:443] is Available
Peers count: 5/5 Connected

Fix

The fix adds a netlink.RouteGet() fallback when go-netroute's Route() fails. netlink.RouteGet() asks the kernel to perform the actual routing decision, respecting policy routing rules (ip rule) and all tables — exactly matching what happens for real packets. vishvananda/netlink is already a dependency.

Tested and confirmed working — all peers switch from Relayed to P2P after applying this patch on the UXG-Max.

  1. sharedsock/sock_linux.go — SharedSocket.WriteTo() fallback

Add import:
"github.com/vishvananda/netlink"

Modify WriteTo() route lookup (around line 316):

_, _, src, err := s.router.Route(rUDPAddr.IP)
if err != nil {
// Fallback to netlink.RouteGet() which respects policy routing tables.
// go-netroute only reads the main table, but devices like UniFi gateways
// keep the default route in a separate policy routing table.
src, err = routeGetSource(rUDPAddr.IP)
if err != nil {
return 0, fmt.Errorf("got an error while checking route, err: %w", err)
}
}

Add helper function:

// routeGetSource uses netlink.RouteGet to determine the preferred source IP for a destination.
// This respects policy routing tables (ip rule), unlike go-netroute which only reads the main table.
func routeGetSource(dst net.IP) (net.IP, error) {
routes, err := netlink.RouteGet(dst)
if err != nil {
return nil, fmt.Errorf("netlink.RouteGet(%s): %w", dst, err)
}
if len(routes) == 0 {
return nil, fmt.Errorf("no route to %s", dst)
}
src := routes[0].Src
if src == nil {
return nil, fmt.Errorf("no source address for route to %s", dst)
}
log.Debugf("Policy routing fallback: route to %s via src %s (table %d, iface idx %d)", dst, src, routes[0].Table, routes[0].LinkIndex)
return src, nil
}

  1. client/internal/routemanager/systemops/systemops_generic.go — GetNextHop() fallback

Modify GetNextHop() (around line 347):

intf, gateway, preferredSrc, err := r.Route(ip.AsSlice())
if err != nil {
// Fallback to netlink.RouteGet() which respects policy routing tables.
log.Debugf("Failed to get route for %s via go-netroute: %v, trying netlink fallback", ip, err)
nexthop, nlErr := getNextHopViaNetlink(ip)
if nlErr != nil {
log.Debugf("Netlink fallback also failed for %s: %v", ip, nlErr)
return Nexthop{}, vars.ErrRouteNotFound
}
return nexthop, nil
}

  1. New file: client/internal/routemanager/systemops/route_fallback_linux.go

//go:build linux && !android

package systemops

import (
"fmt"
"net"
"net/netip"

log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"

)

func getNextHopViaNetlink(ip netip.Addr) (Nexthop, error) {
dst := ip.AsSlice()
routes, err := netlink.RouteGet(net.IP(dst))
if err != nil {
return Nexthop{}, fmt.Errorf("netlink.RouteGet(%s): %w", ip, err)
}
if len(routes) == 0 {
return Nexthop{}, fmt.Errorf("no route to %s via netlink", ip)
}

route := routes[0]

var intf *net.Interface
if route.LinkIndex > 0 {
    intf, err = net.InterfaceByIndex(route.LinkIndex)
    if err != nil {
        log.Debugf("Failed to get interface for index %d: %v", route.LinkIndex, err)
    }
}

if route.Gw != nil {
    addr, err := ipToAddr(route.Gw, intf)
    if err != nil {
        return Nexthop{}, fmt.Errorf("convert gateway to address: %w", err)
    }
    log.Debugf("Policy routing fallback: route to %s via gw %s (table %d, iface %v)", ip, route.Gw, route.Table, intf)
    return Nexthop{IP: addr, Intf: intf}, nil
}

if route.Src != nil {
    addr, err := ipToAddr(route.Src, intf)
    if err != nil {
        return Nexthop{}, fmt.Errorf("convert source to address: %w", err)
    }
    log.Debugf("Policy routing fallback: route to %s via src %s (table %d, iface %v)", ip, route.Src, route.Table, intf)
    return Nexthop{IP: addr, Intf: intf}, nil
}

return Nexthop{Intf: intf}, nil

}

  1. New file: client/internal/routemanager/systemops/route_fallback_other.go

//go:build !linux || android

package systemops

import (
"fmt"
"net/netip"
)

func getNextHopViaNetlink(ip netip.Addr) (Nexthop, error) {
return Nexthop{}, fmt.Errorf("netlink route fallback not available on this platform")
}

• The fallback only activates when go-netroute fails — devices with standard routing (default route in main table) are unaffected
• vishvananda/netlink is already a dependency in go.mod
• netlink.RouteGet() is the standard way to query the kernel's routing decision on Linux and is used elsewhere in the codebase

Affected platforms

Any device using policy-based routing where the default route is not in the main table:
• Ubiquiti UXG-Max (confirmed fix works)
• Ubiquiti UXG-Enterprise (same routing architecture)
• Ubiquiti UDM Pro (firmware 4.0+)
• Likely all UniFi OS gateway devices
• OpenWRT devices with mwan3 or custom policy routing
• Any embedded Linux router with multi-WAN / policy routing

<!-- gh-comment-id:4107961769 --> @thvevirtue commented on GitHub (Mar 23, 2026): ICE/P2P fails on devices with policy-based routing (UniFi gateways, etc.) — go-netroute only queries main table NetBird version 0.66.4 Environment • Device: Ubiquiti UXG-Max • Kernel: 5.4.213-ui-ipq5322 (ARM64, Qualcomm IPQ5322) • OS: Debian GNU/Linux 11 (bullseye) • iptables tables available: filter, nat, mangle, raw • Self-hosted management server Describe the problem On the Ubiquiti UXG-Max (and likely all UniFi gateway devices), all peer connections remain Relayed — P2P/ICE never establishes. Management, Signal, and Relay connect successfully with NB_USE_LEGACY_ROUTING=true. Root cause: The UXG-Max uses policy-based routing where the default gateway lives in a custom routing table (201.eth4), not in the main table: # ip route show table main 192.168.1.0/24 dev br0 proto kernel scope link src 192.168.1.1 (... only directly connected subnets, NO default route) # ip route show table 201.eth4 default via <gateway> dev eth4 proto dhcp metric 200 # ip rule show 0: from all lookup local 32000: from all lookup main 32501: from all fwmark 0x1a0000/0x7e0000 lookup 201.eth4 32766: from all lookup 201.eth4 32767: from all lookup default NetBird uses go-netroute (github.com/libp2p/go-netroute, forked at github.com/netbirdio/go-netroute) for route lookups. This library calls syscall.NetlinkRIB(RTM_GETROUTE, AF_UNSPEC) which only dumps routes from the main table (254). Since there's no default route in main, all route lookups for external IPs fail. This breaks two code paths: 1. SharedSocket.WriteTo() (sharedsock/sock_linux.go:316) — calls s.router.Route() to determine source IP for raw UDP packets. Fails with "Route referenced unknown interface". 2. GetNextHop() (client/internal/routemanager/systemops/systemops_generic.go:346) — called via write hooks when ICE sends packets. Same failure. Result: Every STUN and ICE connectivity-check packet is dropped before reaching the kernel. Local ICE candidates are gathered successfully and remote candidates are received from peers, but no packets can ever be sent. ICE checking always times out and falls back to relay. Normal TCP traffic (gRPC to management/signal, WS to relay) works fine because standard Go net.Dial lets the kernel handle routing through policy rules. Logs (key lines) INFO Using legacy routing setup WARN failed to initialize ebpf proxy, fallback to user space proxy: bpf_link not supported (requires >= v5.7) # ICE agents start and gather local candidates successfully: DEBG worker_ice.go: discovered local candidate udp4 host <local_ip>:51820 # But STUN fails — route check can't find route in main table: ice WARNING: Failed get server reflexive address udp4 stun:<server>:3479: failed to send STUN packet: got an error while checking route, err: Route referenced unknown interface # All ICE connectivity checks fail the same way: DEBG systemops_generic.go:348: Failed to get route for <peer_ip>: Route referenced unknown interface ice INFO: Failed to send packet: got an error while checking route, err: Route referenced unknown interface (repeated hundreds of times for every peer) netbird status -d output Peers detail: peer1.example.vpn: Connection type: Relayed ICE candidate (Local/Remote): -/- ICE candidate endpoints (Local/Remote): -/- Relay server address: rels://<server>:443 (all 5 peers: Relayed, ICE -/-) Management: Connected Signal: Connected Relays: [stun:<server>:3479] is Available, [rels://<server>:443] is Available Peers count: 5/5 Connected Fix The fix adds a netlink.RouteGet() fallback when go-netroute's Route() fails. netlink.RouteGet() asks the kernel to perform the actual routing decision, respecting policy routing rules (ip rule) and all tables — exactly matching what happens for real packets. vishvananda/netlink is already a dependency. Tested and confirmed working — all peers switch from Relayed to P2P after applying this patch on the UXG-Max. 1. sharedsock/sock_linux.go — SharedSocket.WriteTo() fallback Add import: "github.com/vishvananda/netlink" Modify WriteTo() route lookup (around line 316): _, _, src, err := s.router.Route(rUDPAddr.IP) if err != nil { // Fallback to netlink.RouteGet() which respects policy routing tables. // go-netroute only reads the main table, but devices like UniFi gateways // keep the default route in a separate policy routing table. src, err = routeGetSource(rUDPAddr.IP) if err != nil { return 0, fmt.Errorf("got an error while checking route, err: %w", err) } } Add helper function: // routeGetSource uses netlink.RouteGet to determine the preferred source IP for a destination. // This respects policy routing tables (ip rule), unlike go-netroute which only reads the main table. func routeGetSource(dst net.IP) (net.IP, error) { routes, err := netlink.RouteGet(dst) if err != nil { return nil, fmt.Errorf("netlink.RouteGet(%s): %w", dst, err) } if len(routes) == 0 { return nil, fmt.Errorf("no route to %s", dst) } src := routes[0].Src if src == nil { return nil, fmt.Errorf("no source address for route to %s", dst) } log.Debugf("Policy routing fallback: route to %s via src %s (table %d, iface idx %d)", dst, src, routes[0].Table, routes[0].LinkIndex) return src, nil } 2. client/internal/routemanager/systemops/systemops_generic.go — GetNextHop() fallback Modify GetNextHop() (around line 347): intf, gateway, preferredSrc, err := r.Route(ip.AsSlice()) if err != nil { // Fallback to netlink.RouteGet() which respects policy routing tables. log.Debugf("Failed to get route for %s via go-netroute: %v, trying netlink fallback", ip, err) nexthop, nlErr := getNextHopViaNetlink(ip) if nlErr != nil { log.Debugf("Netlink fallback also failed for %s: %v", ip, nlErr) return Nexthop{}, vars.ErrRouteNotFound } return nexthop, nil } 3. New file: client/internal/routemanager/systemops/route_fallback_linux.go //go:build linux && !android package systemops import ( "fmt" "net" "net/netip" log "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" ) func getNextHopViaNetlink(ip netip.Addr) (Nexthop, error) { dst := ip.AsSlice() routes, err := netlink.RouteGet(net.IP(dst)) if err != nil { return Nexthop{}, fmt.Errorf("netlink.RouteGet(%s): %w", ip, err) } if len(routes) == 0 { return Nexthop{}, fmt.Errorf("no route to %s via netlink", ip) } route := routes[0] var intf *net.Interface if route.LinkIndex > 0 { intf, err = net.InterfaceByIndex(route.LinkIndex) if err != nil { log.Debugf("Failed to get interface for index %d: %v", route.LinkIndex, err) } } if route.Gw != nil { addr, err := ipToAddr(route.Gw, intf) if err != nil { return Nexthop{}, fmt.Errorf("convert gateway to address: %w", err) } log.Debugf("Policy routing fallback: route to %s via gw %s (table %d, iface %v)", ip, route.Gw, route.Table, intf) return Nexthop{IP: addr, Intf: intf}, nil } if route.Src != nil { addr, err := ipToAddr(route.Src, intf) if err != nil { return Nexthop{}, fmt.Errorf("convert source to address: %w", err) } log.Debugf("Policy routing fallback: route to %s via src %s (table %d, iface %v)", ip, route.Src, route.Table, intf) return Nexthop{IP: addr, Intf: intf}, nil } return Nexthop{Intf: intf}, nil } 4. New file: client/internal/routemanager/systemops/route_fallback_other.go //go:build !linux || android package systemops import ( "fmt" "net/netip" ) func getNextHopViaNetlink(ip netip.Addr) (Nexthop, error) { return Nexthop{}, fmt.Errorf("netlink route fallback not available on this platform") } • The fallback only activates when go-netroute fails — devices with standard routing (default route in main table) are unaffected • vishvananda/netlink is already a dependency in go.mod • netlink.RouteGet() is the standard way to query the kernel's routing decision on Linux and is used elsewhere in the codebase Affected platforms Any device using policy-based routing where the default route is not in the main table: • Ubiquiti UXG-Max (confirmed fix works) • Ubiquiti UXG-Enterprise (same routing architecture) • Ubiquiti UDM Pro (firmware 4.0+) • Likely all UniFi OS gateway devices • OpenWRT devices with mwan3 or custom policy routing • Any embedded Linux router with multi-WAN / policy routing
Sign in to join this conversation.
No Label triage-needed
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#10882