Files
netbird/client/net/net_darwin_test.go
2026-04-20 07:51:24 +00:00

286 lines
7.9 KiB
Go

//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)
}