mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-23 02:36:42 +00:00
Compare commits
1 Commits
fix/manage
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1eaf9e0d |
125
client/internal/routemanager/systemops/systemops_darwin_test.go
Normal file
125
client/internal/routemanager/systemops/systemops_darwin_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
//go:build darwin && !ios
|
||||||
|
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAfOf verifies that afOf returns the correct string for each address family.
|
||||||
|
func TestAfOf(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr netip.Addr
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "IPv4 unspecified",
|
||||||
|
addr: netip.IPv4Unspecified(),
|
||||||
|
want: "IPv4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 private",
|
||||||
|
addr: netip.MustParseAddr("10.0.0.1"),
|
||||||
|
want: "IPv4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 loopback",
|
||||||
|
addr: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
want: "IPv4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 unspecified",
|
||||||
|
addr: netip.IPv6Unspecified(),
|
||||||
|
want: "IPv6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 loopback",
|
||||||
|
addr: netip.MustParseAddr("::1"),
|
||||||
|
want: "IPv6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 unicast",
|
||||||
|
addr: netip.MustParseAddr("2001:db8::1"),
|
||||||
|
want: "IPv6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 link-local",
|
||||||
|
addr: netip.MustParseAddr("fe80::1"),
|
||||||
|
want: "IPv6",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, afOf(tt.addr))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_AdvancedRoutingBypassesTunnelLookup verifies that when
|
||||||
|
// AdvancedRouting is active, IsAddrRouted immediately returns (false, zero)
|
||||||
|
// regardless of the provided vpn routes, because the WG socket is bound to
|
||||||
|
// the physical interface via IP_BOUND_IF and bypasses the main routing table.
|
||||||
|
func TestIsAddrRouted_AdvancedRoutingBypassesTunnelLookup(t *testing.T) {
|
||||||
|
// On darwin, AdvancedRouting returns true unless overridden.
|
||||||
|
// Ensure we reset the state after the test.
|
||||||
|
t.Setenv("NB_USE_LEGACY_ROUTING", "false")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
nbnet.Init()
|
||||||
|
|
||||||
|
require.True(t, nbnet.AdvancedRouting(), "test requires advanced routing to be active on darwin")
|
||||||
|
|
||||||
|
vpnRoutes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr netip.Addr
|
||||||
|
}{
|
||||||
|
{"IPv4 in VPN route", netip.MustParseAddr("10.0.0.1")},
|
||||||
|
{"IPv4 in narrow VPN route", netip.MustParseAddr("192.168.1.100")},
|
||||||
|
{"IPv4 default route covered", netip.MustParseAddr("8.8.8.8")},
|
||||||
|
{"IPv6 in VPN route", netip.MustParseAddr("2001:db8::1")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
routed, prefix := IsAddrRouted(tt.addr, vpnRoutes)
|
||||||
|
assert.False(t, routed, "should not be marked as routed via VPN when advanced routing is active")
|
||||||
|
assert.Equal(t, netip.Prefix{}, prefix, "matched prefix should be zero when advanced routing is active")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_LegacyModeFallsThroughToTable verifies that when
|
||||||
|
// NB_USE_LEGACY_ROUTING=true disables advanced routing, IsAddrRouted
|
||||||
|
// performs the normal VPN-route vs local-route comparison.
|
||||||
|
func TestIsAddrRouted_LegacyModeFallsThroughToTable(t *testing.T) {
|
||||||
|
t.Setenv("NB_USE_LEGACY_ROUTING", "true")
|
||||||
|
nbnet.Init()
|
||||||
|
|
||||||
|
require.False(t, nbnet.AdvancedRouting(), "test requires advanced routing to be disabled")
|
||||||
|
|
||||||
|
// Use an address that is very unlikely to exist in the host routing table
|
||||||
|
// as a local route, so the VPN route wins.
|
||||||
|
vpnRoutes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("198.51.100.0/24"), // TEST-NET-2 – not in normal routing tables
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := netip.MustParseAddr("198.51.100.1")
|
||||||
|
routed, _ := IsAddrRouted(addr, vpnRoutes)
|
||||||
|
// We cannot assert a specific outcome because it depends on the host's
|
||||||
|
// routing table, but we CAN assert that the call did not panic and returned
|
||||||
|
// a consistent pair.
|
||||||
|
_ = routed
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//go:build !android && !ios
|
||||||
|
|
||||||
|
package systemops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// withLegacyRouting forcibly puts the nbnet package into legacy (non-advanced)
|
||||||
|
// routing mode for the duration of the test, then restores the previous value.
|
||||||
|
func withLegacyRouting(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
t.Setenv("NB_USE_LEGACY_ROUTING", "true")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
nbnet.Init()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// After the test, re-initialise with no overrides so that subsequent
|
||||||
|
// tests start with a clean state.
|
||||||
|
nbnet.Init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// withAdvancedRouting attempts to enable advanced routing for the test.
|
||||||
|
// On platforms where Init() cannot produce AdvancedRouting()=true (e.g. Linux
|
||||||
|
// without root), the calling test is skipped.
|
||||||
|
func withAdvancedRouting(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
t.Setenv("NB_USE_LEGACY_ROUTING", "false")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
nbnet.Init()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
nbnet.Init()
|
||||||
|
})
|
||||||
|
if !nbnet.AdvancedRouting() {
|
||||||
|
t.Skip("advanced routing not available in this environment (need root or darwin)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_AdvancedRoutingShortCircuit verifies that when
|
||||||
|
// AdvancedRouting() returns true, IsAddrRouted immediately returns
|
||||||
|
// (false, zero prefix) regardless of any VPN routes, because the WG socket is
|
||||||
|
// bound directly to the physical interface and bypasses the kernel routing table.
|
||||||
|
func TestIsAddrRouted_AdvancedRoutingShortCircuit(t *testing.T) {
|
||||||
|
withAdvancedRouting(t)
|
||||||
|
|
||||||
|
vpnRoutes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
|
netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
netip.MustParsePrefix("::/0"),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr string
|
||||||
|
}{
|
||||||
|
{"IPv4 matched by 10/8", "10.0.0.1"},
|
||||||
|
{"IPv4 matched by 192.168/16", "192.168.1.1"},
|
||||||
|
{"IPv4 caught by default", "8.8.8.8"},
|
||||||
|
{"IPv6 caught by default", "2001:db8::1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addr := netip.MustParseAddr(tt.addr)
|
||||||
|
routed, prefix := IsAddrRouted(addr, vpnRoutes)
|
||||||
|
assert.False(t, routed, "advanced routing must short-circuit VPN route check")
|
||||||
|
assert.Equal(t, netip.Prefix{}, prefix, "returned prefix must be zero under advanced routing")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_AdvancedRouting_EmptyRoutes verifies the short-circuit with
|
||||||
|
// an empty vpnRoutes slice — the result must still be (false, zero).
|
||||||
|
func TestIsAddrRouted_AdvancedRouting_EmptyRoutes(t *testing.T) {
|
||||||
|
withAdvancedRouting(t)
|
||||||
|
|
||||||
|
routed, prefix := IsAddrRouted(netip.MustParseAddr("10.0.0.1"), nil)
|
||||||
|
assert.False(t, routed)
|
||||||
|
assert.Equal(t, netip.Prefix{}, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_LegacyMode_NonVPNAddress verifies that under legacy routing
|
||||||
|
// an address that is not covered by any VPN prefix returns false.
|
||||||
|
func TestIsAddrRouted_LegacyMode_NonVPNAddress(t *testing.T) {
|
||||||
|
withLegacyRouting(t)
|
||||||
|
|
||||||
|
// 198.51.100.x (TEST-NET-2) is not normally in any routing table.
|
||||||
|
vpnRoutes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := netip.MustParseAddr("198.51.100.1")
|
||||||
|
routed, _ := IsAddrRouted(addr, vpnRoutes)
|
||||||
|
assert.False(t, routed, "address not in VPN routes should not be marked as VPN-routed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_LegacyMode_EmptyVPNRoutes verifies that an empty vpnRoutes
|
||||||
|
// slice always yields (false, zero prefix) even under legacy routing.
|
||||||
|
func TestIsAddrRouted_LegacyMode_EmptyVPNRoutes(t *testing.T) {
|
||||||
|
withLegacyRouting(t)
|
||||||
|
|
||||||
|
routed, prefix := IsAddrRouted(netip.MustParseAddr("10.0.0.1"), nil)
|
||||||
|
assert.False(t, routed)
|
||||||
|
assert.Equal(t, netip.Prefix{}, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsAddrRouted_AdvancedVsLegacy_ContrastiveBehaviour documents the
|
||||||
|
// contract difference between the two modes: with a VPN default route and an
|
||||||
|
// address that matches it, legacy mode may mark it as VPN-routed while advanced
|
||||||
|
// mode must never do so.
|
||||||
|
func TestIsAddrRouted_AdvancedVsLegacy_ContrastiveBehaviour(t *testing.T) {
|
||||||
|
vpnRoutes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
}
|
||||||
|
addr := netip.MustParseAddr("8.8.8.8")
|
||||||
|
|
||||||
|
// --- advanced routing: must always return false ---
|
||||||
|
t.Run("advanced routing", func(t *testing.T) {
|
||||||
|
withAdvancedRouting(t)
|
||||||
|
routed, prefix := IsAddrRouted(addr, vpnRoutes)
|
||||||
|
assert.False(t, routed, "advanced routing must bypass VPN route lookup")
|
||||||
|
assert.Equal(t, netip.Prefix{}, prefix)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- legacy routing: delegates to kernel table check, does not panic ---
|
||||||
|
t.Run("legacy routing", func(t *testing.T) {
|
||||||
|
withLegacyRouting(t)
|
||||||
|
// We don't assert true/false here because it depends on the host
|
||||||
|
// routing table, but the call must not panic and must return
|
||||||
|
// a valid (bool, prefix) pair.
|
||||||
|
routed, prefix := IsAddrRouted(addr, vpnRoutes)
|
||||||
|
t.Logf("legacy IsAddrRouted(%s, %v) = (%v, %v)", addr, vpnRoutes, routed, prefix)
|
||||||
|
})
|
||||||
|
}
|
||||||
121
client/net/env_bound_iface_test.go
Normal file
121
client/net/env_bound_iface_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//go:build (darwin && !ios) || windows
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resetAdvancedRoutingState resets the package-level advancedRoutingSupported variable
|
||||||
|
// and cleans up after the test.
|
||||||
|
func resetAdvancedRoutingState(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
orig := advancedRoutingSupported
|
||||||
|
t.Cleanup(func() { advancedRoutingSupported = orig })
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckAdvancedRoutingSupport_LegacyRoutingTrue verifies that setting
|
||||||
|
// NB_USE_LEGACY_ROUTING=true disables advanced routing.
|
||||||
|
func TestCheckAdvancedRoutingSupport_LegacyRoutingTrue(t *testing.T) {
|
||||||
|
t.Setenv(envUseLegacyRouting, "true")
|
||||||
|
assert.False(t, checkAdvancedRoutingSupport())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckAdvancedRoutingSupport_LegacyRoutingFalse verifies that
|
||||||
|
// NB_USE_LEGACY_ROUTING=false still allows advanced routing when netstack is off.
|
||||||
|
func TestCheckAdvancedRoutingSupport_LegacyRoutingFalse(t *testing.T) {
|
||||||
|
t.Setenv(envUseLegacyRouting, "false")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
assert.True(t, checkAdvancedRoutingSupport())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckAdvancedRoutingSupport_LegacyRoutingInvalid verifies that an invalid
|
||||||
|
// value for NB_USE_LEGACY_ROUTING is ignored (treated as false), so advanced
|
||||||
|
// routing remains enabled when netstack is off.
|
||||||
|
func TestCheckAdvancedRoutingSupport_LegacyRoutingInvalid(t *testing.T) {
|
||||||
|
t.Setenv(envUseLegacyRouting, "notabool")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
// The invalid value is ignored; the default (false) is kept, so advanced routing
|
||||||
|
// is not suppressed by the legacy-routing flag.
|
||||||
|
assert.True(t, checkAdvancedRoutingSupport())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckAdvancedRoutingSupport_NetstackEnabled verifies that netstack mode
|
||||||
|
// disables advanced routing.
|
||||||
|
func TestCheckAdvancedRoutingSupport_NetstackEnabled(t *testing.T) {
|
||||||
|
t.Setenv(envUseLegacyRouting, "false")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "true")
|
||||||
|
assert.False(t, checkAdvancedRoutingSupport())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckAdvancedRoutingSupport_NoEnvVars verifies that with no env overrides
|
||||||
|
// advanced routing is supported (the happy path).
|
||||||
|
func TestCheckAdvancedRoutingSupport_NoEnvVars(t *testing.T) {
|
||||||
|
// Unset both controlling variables so we hit the default path.
|
||||||
|
t.Setenv(envUseLegacyRouting, "")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
assert.True(t, checkAdvancedRoutingSupport())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckAdvancedRoutingSupport_LegacyRoutingEmptyString verifies that an
|
||||||
|
// empty NB_USE_LEGACY_ROUTING is treated as "not set" and does not disable
|
||||||
|
// advanced routing.
|
||||||
|
func TestCheckAdvancedRoutingSupport_LegacyRoutingEmptyString(t *testing.T) {
|
||||||
|
t.Setenv(envUseLegacyRouting, "")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
assert.True(t, checkAdvancedRoutingSupport())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdvancedRouting_ReflectsInit verifies that after calling Init() with
|
||||||
|
// NB_USE_LEGACY_ROUTING=true, AdvancedRouting() returns false.
|
||||||
|
func TestAdvancedRouting_ReflectsInit(t *testing.T) {
|
||||||
|
resetAdvancedRoutingState(t)
|
||||||
|
|
||||||
|
t.Setenv(envUseLegacyRouting, "true")
|
||||||
|
Init()
|
||||||
|
|
||||||
|
assert.False(t, AdvancedRouting(), "AdvancedRouting should return false after Init with legacy routing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdvancedRouting_ReflectsInit_Advanced verifies that after calling Init()
|
||||||
|
// without legacy overrides, AdvancedRouting() returns true.
|
||||||
|
func TestAdvancedRouting_ReflectsInit_Advanced(t *testing.T) {
|
||||||
|
resetAdvancedRoutingState(t)
|
||||||
|
|
||||||
|
t.Setenv(envUseLegacyRouting, "false")
|
||||||
|
t.Setenv("NB_USE_NETSTACK_MODE", "false")
|
||||||
|
Init()
|
||||||
|
|
||||||
|
assert.True(t, AdvancedRouting(), "AdvancedRouting should return true after Init without legacy overrides")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetAndGetVPNInterfaceName verifies SetVPNInterfaceName and GetVPNInterfaceName
|
||||||
|
// are consistent.
|
||||||
|
func TestSetAndGetVPNInterfaceName(t *testing.T) {
|
||||||
|
orig := GetVPNInterfaceName()
|
||||||
|
t.Cleanup(func() { SetVPNInterfaceName(orig) })
|
||||||
|
|
||||||
|
SetVPNInterfaceName("utun3")
|
||||||
|
assert.Equal(t, "utun3", GetVPNInterfaceName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetVPNInterfaceName_Empty verifies that setting an empty name is accepted.
|
||||||
|
func TestSetVPNInterfaceName_Empty(t *testing.T) {
|
||||||
|
orig := GetVPNInterfaceName()
|
||||||
|
t.Cleanup(func() { SetVPNInterfaceName(orig) })
|
||||||
|
|
||||||
|
SetVPNInterfaceName("")
|
||||||
|
assert.Equal(t, "", GetVPNInterfaceName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetVPNInterfaceName_OverwritesPrevious verifies that the second call wins.
|
||||||
|
func TestSetVPNInterfaceName_OverwritesPrevious(t *testing.T) {
|
||||||
|
orig := GetVPNInterfaceName()
|
||||||
|
t.Cleanup(func() { SetVPNInterfaceName(orig) })
|
||||||
|
|
||||||
|
SetVPNInterfaceName("utun1")
|
||||||
|
SetVPNInterfaceName("utun9")
|
||||||
|
assert.Equal(t, "utun9", GetVPNInterfaceName())
|
||||||
|
}
|
||||||
39
client/net/env_mobile_test.go
Normal file
39
client/net/env_mobile_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//go:build ios || android
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAdvancedRouting_Mobile verifies that AdvancedRouting always returns true
|
||||||
|
// on mobile platforms.
|
||||||
|
func TestAdvancedRouting_Mobile(t *testing.T) {
|
||||||
|
assert.True(t, AdvancedRouting(), "AdvancedRouting must always be true on mobile")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInit_Mobile verifies that Init is a no-op and does not panic.
|
||||||
|
func TestInit_Mobile(t *testing.T) {
|
||||||
|
// Should not panic.
|
||||||
|
Init()
|
||||||
|
// After Init, AdvancedRouting must still return true.
|
||||||
|
assert.True(t, AdvancedRouting())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetVPNInterfaceName_Mobile verifies that SetVPNInterfaceName is a no-op
|
||||||
|
// and does not panic.
|
||||||
|
func TestSetVPNInterfaceName_Mobile(t *testing.T) {
|
||||||
|
// Should not panic for any input.
|
||||||
|
SetVPNInterfaceName("utun0")
|
||||||
|
SetVPNInterfaceName("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetVPNInterfaceName_Mobile verifies that GetVPNInterfaceName always
|
||||||
|
// returns an empty string on mobile.
|
||||||
|
func TestGetVPNInterfaceName_Mobile(t *testing.T) {
|
||||||
|
// Even after a SetVPNInterfaceName call (no-op), the getter returns "".
|
||||||
|
SetVPNInterfaceName("utun0")
|
||||||
|
assert.Equal(t, "", GetVPNInterfaceName(), "GetVPNInterfaceName must return empty string on mobile")
|
||||||
|
}
|
||||||
286
client/net/net_darwin_test.go
Normal file
286
client/net/net_darwin_test.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
//go:build darwin && !ios
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resetBoundIfaces clears global state before each test that manipulates it.
|
||||||
|
func resetBoundIfaces(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
ClearBoundInterfaces()
|
||||||
|
t.Cleanup(ClearBoundInterfaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsV6Network verifies the isV6Network helper correctly identifies v6 networks.
|
||||||
|
func TestIsV6Network(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
network string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"tcp6", true},
|
||||||
|
{"udp6", true},
|
||||||
|
{"ip6", true},
|
||||||
|
{"tcp", false},
|
||||||
|
{"udp", false},
|
||||||
|
{"tcp4", false},
|
||||||
|
{"udp4", false},
|
||||||
|
{"ip4", false},
|
||||||
|
{"ip", false},
|
||||||
|
{"", false},
|
||||||
|
// Arbitrary suffix-6 strings
|
||||||
|
{"unix6", true},
|
||||||
|
{"custom6", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.network, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, isV6Network(tt.network))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetBoundInterface_AFINET sets boundIface4 via AF_INET.
|
||||||
|
func TestSetBoundInterface_AFINET(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
iface := &net.Interface{Index: 5, Name: "en0"}
|
||||||
|
SetBoundInterface(unix.AF_INET, iface)
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
got := boundIface4
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, 5, got.Index)
|
||||||
|
assert.Equal(t, "en0", got.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetBoundInterface_AFINET6 sets boundIface6 via AF_INET6.
|
||||||
|
func TestSetBoundInterface_AFINET6(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
iface := &net.Interface{Index: 7, Name: "utun0"}
|
||||||
|
SetBoundInterface(unix.AF_INET6, iface)
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
got := boundIface6
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, 7, got.Index)
|
||||||
|
assert.Equal(t, "utun0", got.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetBoundInterface_Nil verifies nil iface is rejected without panicking.
|
||||||
|
func TestSetBoundInterface_Nil(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
// Should not panic, just log a warning and leave existing values untouched.
|
||||||
|
iface := &net.Interface{Index: 1, Name: "en1"}
|
||||||
|
SetBoundInterface(unix.AF_INET, iface)
|
||||||
|
|
||||||
|
SetBoundInterface(unix.AF_INET, nil)
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
got := boundIface4
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
// Value must be unchanged after the rejected nil write.
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, 1, got.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetBoundInterface_UnknownAF verifies unknown address families are ignored.
|
||||||
|
func TestSetBoundInterface_UnknownAF(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
iface := &net.Interface{Index: 3, Name: "en2"}
|
||||||
|
// Use an address family that is not AF_INET or AF_INET6.
|
||||||
|
SetBoundInterface(99, iface)
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
v4, v6 := boundIface4, boundIface6
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
assert.Nil(t, v4, "unknown AF must not populate boundIface4")
|
||||||
|
assert.Nil(t, v6, "unknown AF must not populate boundIface6")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClearBoundInterfaces clears both cached interfaces.
|
||||||
|
func TestClearBoundInterfaces(t *testing.T) {
|
||||||
|
iface4 := &net.Interface{Index: 1, Name: "en0"}
|
||||||
|
iface6 := &net.Interface{Index: 2, Name: "en0"}
|
||||||
|
|
||||||
|
SetBoundInterface(unix.AF_INET, iface4)
|
||||||
|
SetBoundInterface(unix.AF_INET6, iface6)
|
||||||
|
|
||||||
|
ClearBoundInterfaces()
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
v4, v6 := boundIface4, boundIface6
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
assert.Nil(t, v4, "boundIface4 must be nil after clear")
|
||||||
|
assert.Nil(t, v6, "boundIface6 must be nil after clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClearBoundInterfaces_Idempotent verifies clearing twice does not panic.
|
||||||
|
func TestClearBoundInterfaces_Idempotent(t *testing.T) {
|
||||||
|
ClearBoundInterfaces()
|
||||||
|
ClearBoundInterfaces()
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
v4, v6 := boundIface4, boundIface6
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
assert.Nil(t, v4)
|
||||||
|
assert.Nil(t, v6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBoundInterfaceFor_PreferSameFamily verifies v4 iface returned for "tcp" and
|
||||||
|
// v6 iface returned for "tcp6" when both slots are populated.
|
||||||
|
func TestBoundInterfaceFor_PreferSameFamily(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
en0 := &net.Interface{Index: 1, Name: "en0"}
|
||||||
|
en1 := &net.Interface{Index: 2, Name: "en1"}
|
||||||
|
SetBoundInterface(unix.AF_INET, en0)
|
||||||
|
SetBoundInterface(unix.AF_INET6, en1)
|
||||||
|
|
||||||
|
got4 := boundInterfaceFor("tcp", "1.2.3.4:80")
|
||||||
|
require.NotNil(t, got4)
|
||||||
|
assert.Equal(t, "en0", got4.Name, "tcp should prefer v4 interface")
|
||||||
|
|
||||||
|
got6 := boundInterfaceFor("tcp6", "[::1]:80")
|
||||||
|
require.NotNil(t, got6)
|
||||||
|
assert.Equal(t, "en1", got6.Name, "tcp6 should prefer v6 interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBoundInterfaceFor_FallbackToOtherFamily returns the other family's iface
|
||||||
|
// when the preferred slot is empty.
|
||||||
|
func TestBoundInterfaceFor_FallbackToOtherFamily(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
// Only v4 populated.
|
||||||
|
en0 := &net.Interface{Index: 1, Name: "en0"}
|
||||||
|
SetBoundInterface(unix.AF_INET, en0)
|
||||||
|
|
||||||
|
// Asking for v6 should fall back to en0.
|
||||||
|
got := boundInterfaceFor("tcp6", "[::1]:80")
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "en0", got.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBoundInterfaceFor_BothEmpty returns nil when both slots are empty.
|
||||||
|
func TestBoundInterfaceFor_BothEmpty(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
got := boundInterfaceFor("tcp", "1.2.3.4:80")
|
||||||
|
assert.Nil(t, got)
|
||||||
|
|
||||||
|
got6 := boundInterfaceFor("tcp6", "[::1]:80")
|
||||||
|
assert.Nil(t, got6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZoneInterface_Empty returns nil for empty address.
|
||||||
|
func TestZoneInterface_Empty(t *testing.T) {
|
||||||
|
iface := zoneInterface("")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZoneInterface_NoZone returns nil when address has no zone.
|
||||||
|
func TestZoneInterface_NoZone(t *testing.T) {
|
||||||
|
// Regular IPv4 address with port — no zone identifier.
|
||||||
|
iface := zoneInterface("192.168.1.1:80")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
|
||||||
|
// Regular IPv6 address with port — no zone identifier.
|
||||||
|
iface = zoneInterface("[2001:db8::1]:80")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
|
||||||
|
// Plain IPv6 address without port or zone.
|
||||||
|
iface = zoneInterface("2001:db8::1")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZoneInterface_InvalidAddress returns nil for completely invalid strings.
|
||||||
|
func TestZoneInterface_InvalidAddress(t *testing.T) {
|
||||||
|
iface := zoneInterface("not-an-address")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
|
||||||
|
iface = zoneInterface("::::")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZoneInterface_NonExistentZoneName returns nil for a zone name that does
|
||||||
|
// not correspond to a real interface on the host.
|
||||||
|
func TestZoneInterface_NonExistentZoneName(t *testing.T) {
|
||||||
|
// Use an interface name that is very unlikely to exist.
|
||||||
|
iface := zoneInterface("fe80::1%nonexistentiface99999")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZoneInterface_NonExistentZoneIndex returns nil for a zone expressed as an
|
||||||
|
// integer index that is not in use.
|
||||||
|
func TestZoneInterface_NonExistentZoneIndex(t *testing.T) {
|
||||||
|
// Interface index 999999 should not exist on any test machine.
|
||||||
|
iface := zoneInterface("fe80::1%999999")
|
||||||
|
assert.Nil(t, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBoundInterfaceFor_SetThenClear verifies that clearing state causes
|
||||||
|
// boundInterfaceFor to return nil afterwards.
|
||||||
|
func TestBoundInterfaceFor_SetThenClear(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
en0 := &net.Interface{Index: 1, Name: "en0"}
|
||||||
|
SetBoundInterface(unix.AF_INET, en0)
|
||||||
|
|
||||||
|
got := boundInterfaceFor("tcp", "1.2.3.4:80")
|
||||||
|
require.NotNil(t, got, "should return iface while set")
|
||||||
|
|
||||||
|
ClearBoundInterfaces()
|
||||||
|
|
||||||
|
got = boundInterfaceFor("tcp", "1.2.3.4:80")
|
||||||
|
assert.Nil(t, got, "should return nil after clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetBoundInterface_OverwritesPreviousValue verifies that calling
|
||||||
|
// SetBoundInterface again updates the stored pointer.
|
||||||
|
func TestSetBoundInterface_OverwritesPreviousValue(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
first := &net.Interface{Index: 1, Name: "en0"}
|
||||||
|
second := &net.Interface{Index: 3, Name: "en1"}
|
||||||
|
|
||||||
|
SetBoundInterface(unix.AF_INET, first)
|
||||||
|
SetBoundInterface(unix.AF_INET, second)
|
||||||
|
|
||||||
|
boundIfaceMu.RLock()
|
||||||
|
got := boundIface4
|
||||||
|
boundIfaceMu.RUnlock()
|
||||||
|
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "en1", got.Name, "second call should overwrite first")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBoundInterfaceFor_OnlyV6Populated returns v6 iface for v4 network
|
||||||
|
// when only v6 slot is filled.
|
||||||
|
func TestBoundInterfaceFor_OnlyV6Populated(t *testing.T) {
|
||||||
|
resetBoundIfaces(t)
|
||||||
|
|
||||||
|
en1 := &net.Interface{Index: 2, Name: "en1"}
|
||||||
|
SetBoundInterface(unix.AF_INET6, en1)
|
||||||
|
|
||||||
|
// v4 network, v4 slot empty → should fall back to v6 slot.
|
||||||
|
got := boundInterfaceFor("tcp", "1.2.3.4:80")
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "en1", got.Name)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user