Compare commits

...

3 Commits

Author SHA1 Message Date
coderabbitai[bot]
cb1eaf9e0d CodeRabbit Generated Unit Tests: Add unit tests for PR changes 2026-04-20 07:51:24 +00:00
Nicolas Frati
8ae8f2098f [management] chores: fix lint error on google workspace (#5907)
* chores: fix lint error on google workspace

* chores: updated google api dependency

* update google golang api sdk to latest
2026-04-16 20:02:09 +02:00
Viktor Liu
a39787d679 [infrastructure] Add CrowdSec LAPI container to self-hosted setup script (#5880) 2026-04-16 18:06:38 +02:00
9 changed files with 903 additions and 81 deletions

View 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
}

View File

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

View 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())
}

View 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")
}

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

32
go.mod
View File

@@ -17,12 +17,12 @@ require (
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.9
github.com/vishvananda/netlink v1.3.1
golang.org/x/crypto v0.48.0
golang.org/x/crypto v0.49.0
golang.org/x/sys v0.42.0
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
golang.zx2c4.com/wireguard/windows v0.5.3
google.golang.org/grpc v1.79.3
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
@@ -115,13 +115,13 @@ require (
goauthentik.io/api/v3 v3.2023051.3
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
golang.org/x/mod v0.32.0
golang.org/x/net v0.51.0
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/term v0.40.0
golang.org/x/time v0.14.0
google.golang.org/api v0.257.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.41.0
golang.org/x/time v0.15.0
google.golang.org/api v0.276.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.7
@@ -131,7 +131,7 @@ require (
)
require (
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.1 // indirect
@@ -210,8 +210,8 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
@@ -295,16 +295,16 @@ require (
github.com/zeebo/blake3 v0.2.3 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/image v0.33.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

76
go.sum
View File

@@ -1,5 +1,5 @@
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
@@ -285,10 +285,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw=
github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -664,8 +664,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
@@ -707,8 +707,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
@@ -725,8 +725,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@@ -745,11 +745,11 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -761,8 +761,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -811,8 +811,8 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -824,10 +824,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -839,8 +839,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -851,19 +851,19 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY=
google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@@ -182,6 +182,23 @@ read_enable_proxy() {
return 0
}
read_enable_crowdsec() {
echo "" > /dev/stderr
echo "Do you want to enable CrowdSec IP reputation blocking?" > /dev/stderr
echo "CrowdSec checks client IPs against a community threat intelligence database" > /dev/stderr
echo "and blocks known malicious sources before they reach your services." > /dev/stderr
echo "A local CrowdSec LAPI container will be added to your deployment." > /dev/stderr
echo -n "Enable CrowdSec? [y/N]: " > /dev/stderr
read -r CHOICE < /dev/tty
if [[ "$CHOICE" =~ ^[Yy]$ ]]; then
echo "true"
else
echo "false"
fi
return 0
}
read_traefik_acme_email() {
echo "" > /dev/stderr
echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr
@@ -297,6 +314,10 @@ initialize_default_values() {
# NetBird Proxy configuration
ENABLE_PROXY="false"
PROXY_TOKEN=""
# CrowdSec configuration
ENABLE_CROWDSEC="false"
CROWDSEC_BOUNCER_KEY=""
return 0
}
@@ -325,6 +346,9 @@ configure_reverse_proxy() {
if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then
TRAEFIK_ACME_EMAIL=$(read_traefik_acme_email)
ENABLE_PROXY=$(read_enable_proxy)
if [[ "$ENABLE_PROXY" == "true" ]]; then
ENABLE_CROWDSEC=$(read_enable_crowdsec)
fi
fi
# Handle external Traefik-specific prompts (option 1)
@@ -354,7 +378,7 @@ check_existing_installation() {
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
echo "You can use the following commands:"
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
echo " rm -f docker-compose.yml dashboard.env config.yaml proxy.env traefik-dynamic.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt"
echo " rm -f docker-compose.yml dashboard.env config.yaml proxy.env traefik-dynamic.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt && rm -rf crowdsec/"
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
exit 1
fi
@@ -375,6 +399,9 @@ generate_configuration_files() {
echo "NB_PROXY_TOKEN=placeholder" >> proxy.env
# TCP ServersTransport for PROXY protocol v2 to the proxy backend
render_traefik_dynamic > traefik-dynamic.yaml
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
mkdir -p crowdsec
fi
fi
;;
1)
@@ -417,8 +444,12 @@ start_services_and_show_instructions() {
if [[ "$ENABLE_PROXY" == "true" ]]; then
# Phase 1: Start core services (without proxy)
local core_services="traefik dashboard netbird-server"
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
core_services="$core_services crowdsec"
fi
echo "Starting core services..."
$DOCKER_COMPOSE_COMMAND up -d traefik dashboard netbird-server
$DOCKER_COMPOSE_COMMAND up -d $core_services
sleep 3
wait_management_proxy traefik
@@ -438,7 +469,33 @@ start_services_and_show_instructions() {
echo "Proxy token created successfully."
# Generate proxy.env with the token
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
echo "Registering CrowdSec bouncer..."
local cs_retries=0
while ! $DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli capi status >/dev/null 2>&1; do
cs_retries=$((cs_retries + 1))
if [[ $cs_retries -ge 30 ]]; then
echo "WARNING: CrowdSec did not become ready. Skipping CrowdSec setup." > /dev/stderr
echo "You can register a bouncer manually later with:" > /dev/stderr
echo " docker exec netbird-crowdsec cscli bouncers add netbird-proxy -o raw" > /dev/stderr
ENABLE_CROWDSEC="false"
break
fi
sleep 2
done
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
CROWDSEC_BOUNCER_KEY=$($DOCKER_COMPOSE_COMMAND exec -T crowdsec \
cscli bouncers add netbird-proxy -o raw 2>/dev/null)
if [[ -z "$CROWDSEC_BOUNCER_KEY" ]]; then
echo "WARNING: Failed to create CrowdSec bouncer key. Skipping CrowdSec setup." > /dev/stderr
ENABLE_CROWDSEC="false"
else
echo "CrowdSec bouncer registered."
fi
fi
fi
render_proxy_env > proxy.env
# Start proxy service
@@ -525,11 +582,25 @@ render_docker_compose_traefik_builtin() {
# Generate proxy service section and Traefik dynamic config if enabled
local proxy_service=""
local proxy_volumes=""
local crowdsec_service=""
local crowdsec_volumes=""
local traefik_file_provider=""
local traefik_dynamic_volume=""
if [[ "$ENABLE_PROXY" == "true" ]]; then
traefik_file_provider=' - "--providers.file.filename=/etc/traefik/dynamic.yaml"'
traefik_dynamic_volume=" - ./traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro"
local proxy_depends="
netbird-server:
condition: service_started"
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
proxy_depends="
netbird-server:
condition: service_started
crowdsec:
condition: service_healthy"
fi
proxy_service="
# NetBird Proxy - exposes internal resources to the internet
proxy:
@@ -539,8 +610,7 @@ render_docker_compose_traefik_builtin() {
- 51820:51820/udp
restart: unless-stopped
networks: [netbird]
depends_on:
- netbird-server
depends_on:${proxy_depends}
env_file:
- ./proxy.env
volumes:
@@ -563,6 +633,35 @@ render_docker_compose_traefik_builtin() {
"
proxy_volumes="
netbird_proxy_certs:"
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
crowdsec_service="
crowdsec:
image: crowdsecurity/crowdsec:v1.7.7
container_name: netbird-crowdsec
restart: unless-stopped
networks: [netbird]
environment:
COLLECTIONS: crowdsecurity/linux
volumes:
- ./crowdsec:/etc/crowdsec
- crowdsec_db:/var/lib/crowdsec/data
healthcheck:
test: ["CMD", "cscli", "lapi", "status"]
interval: 10s
timeout: 5s
retries: 15
labels:
- traefik.enable=false
logging:
driver: \"json-file\"
options:
max-size: \"500m\"
max-file: \"2\"
"
crowdsec_volumes="
crowdsec_db:"
fi
fi
cat <<EOF
@@ -675,10 +774,10 @@ $traefik_dynamic_volume
options:
max-size: "500m"
max-file: "2"
${proxy_service}
${proxy_service}${crowdsec_service}
volumes:
netbird_data:
netbird_traefik_letsencrypt:${proxy_volumes}
netbird_traefik_letsencrypt:${proxy_volumes}${crowdsec_volumes}
networks:
netbird:
@@ -783,6 +882,14 @@ NB_PROXY_PROXY_PROTOCOL=true
# Trust Traefik's IP for PROXY protocol headers
NB_PROXY_TRUSTED_PROXIES=$TRAEFIK_IP
EOF
if [[ "$ENABLE_CROWDSEC" == "true" && -n "$CROWDSEC_BOUNCER_KEY" ]]; then
cat <<EOF
NB_PROXY_CROWDSEC_API_URL=http://crowdsec:8080
NB_PROXY_CROWDSEC_API_KEY=$CROWDSEC_BOUNCER_KEY
EOF
fi
return 0
}
@@ -1172,6 +1279,17 @@ print_builtin_traefik_instructions() {
echo ""
echo " *.$NETBIRD_DOMAIN CNAME $NETBIRD_DOMAIN"
echo ""
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
echo "CrowdSec IP Reputation:"
echo " CrowdSec LAPI is running and connected to the community blocklist."
echo " The proxy will automatically check client IPs against known threats."
echo " Enable CrowdSec per-service in the dashboard under Access Control."
echo ""
echo " To enroll in CrowdSec Console (optional, for dashboard and premium blocklists):"
echo " docker exec netbird-crowdsec cscli console enroll <your-enrollment-key>"
echo " Get your enrollment key at: https://app.crowdsec.net"
echo ""
fi
fi
return 0
}

View File

@@ -66,14 +66,14 @@ func NewGoogleWorkspaceManager(ctx context.Context, config GoogleWorkspaceClient
}
// Create a new Admin SDK Directory service client
adminCredentials, err := getGoogleCredentials(ctx, config.ServiceAccountKey)
credentialsOption, err := getGoogleCredentialsOption(ctx, config.ServiceAccountKey)
if err != nil {
return nil, err
}
service, err := admin.NewService(context.Background(),
option.WithScopes(admin.AdminDirectoryUserReadonlyScope),
option.WithCredentials(adminCredentials),
credentialsOption,
)
if err != nil {
return nil, err
@@ -218,39 +218,32 @@ func (gm *GoogleWorkspaceManager) DeleteUser(_ context.Context, userID string) e
return nil
}
// getGoogleCredentials retrieves Google credentials based on the provided serviceAccountKey.
// It decodes the base64-encoded serviceAccountKey and attempts to obtain credentials using it.
// If that fails, it falls back to using the default Google credentials path.
// It returns the retrieved credentials or an error if unsuccessful.
func getGoogleCredentials(ctx context.Context, serviceAccountKey string) (*google.Credentials, error) {
// getGoogleCredentialsOption returns the google.golang.org/api option carrying
// Google credentials derived from the provided serviceAccountKey.
// It decodes the base64-encoded serviceAccountKey and uses it as the credentials JSON.
// If the key is empty, it falls back to the default Google credentials path.
func getGoogleCredentialsOption(ctx context.Context, serviceAccountKey string) (option.ClientOption, error) {
log.WithContext(ctx).Debug("retrieving google credentials from the base64 encoded service account key")
decodeKey, err := base64.StdEncoding.DecodeString(serviceAccountKey)
if err != nil {
return nil, fmt.Errorf("failed to decode service account key: %w", err)
}
creds, err := google.CredentialsFromJSON(
context.Background(),
decodeKey,
admin.AdminDirectoryUserReadonlyScope,
)
if err == nil {
// No need to fallback to the default Google credentials path
return creds, nil
if len(decodeKey) > 0 {
return option.WithAuthCredentialsJSON(option.ServiceAccount, decodeKey), nil
}
log.WithContext(ctx).Debugf("failed to retrieve Google credentials from ServiceAccountKey: %v", err)
log.WithContext(ctx).Debug("falling back to default google credentials location")
log.WithContext(ctx).Debug("no service account key provided, falling back to default google credentials location")
creds, err = google.FindDefaultCredentials(
context.Background(),
creds, err := google.FindDefaultCredentials(
ctx,
admin.AdminDirectoryUserReadonlyScope,
)
if err != nil {
return nil, err
}
return creds, nil
return option.WithCredentials(creds), nil
}
// parseGoogleWorkspaceUser parse google user to UserData.