//go:build !windows package routemanager import ( "net" "net/netip" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" ) // reconcileWGMock is a minimal iface.WGIface that only records AddAllowedIP calls; every other // method is an inert stub because ReconcilePeerAllowedIPs exercises none of them. type reconcileWGMock struct { mu sync.Mutex adds map[string][]netip.Prefix } func (m *reconcileWGMock) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { m.mu.Lock() defer m.mu.Unlock() if m.adds == nil { m.adds = map[string][]netip.Prefix{} } m.adds[peerKey] = append(m.adds[peerKey], allowedIP) return nil } func (m *reconcileWGMock) added(peerKey string) []netip.Prefix { m.mu.Lock() defer m.mu.Unlock() return m.adds[peerKey] } func (m *reconcileWGMock) RemoveAllowedIP(string, netip.Prefix) error { return nil } func (m *reconcileWGMock) Name() string { return "utun-test" } func (m *reconcileWGMock) Address() wgaddr.Address { return wgaddr.Address{} } func (m *reconcileWGMock) ToInterface() *net.Interface { return nil } func (m *reconcileWGMock) IsUserspaceBind() bool { return false } func (m *reconcileWGMock) GetFilter() device.PacketFilter { return nil } func (m *reconcileWGMock) GetDevice() *device.FilteredDevice { return nil } func (m *reconcileWGMock) GetNet() *netstack.Net { return nil } // TestReconcilePeerAllowedIPs verifies the declarative reconcile re-applies every routed prefix // tracked for the peer (self-heal, independent of refcount level) and stays scoped to that peer. func TestReconcilePeerAllowedIPs(t *testing.T) { wg := &reconcileWGMock{} m := &DefaultManager{wgInterface: wg} m.allowedIPsRefCounter = refcounter.NewAllowedIPs( func(_ netip.Prefix, peerKey string) (string, error) { return peerKey, nil }, func(netip.Prefix, string) error { return nil }, ) peerA1 := netip.MustParsePrefix("10.0.0.0/24") peerA2 := netip.MustParsePrefix("10.1.0.0/24") peerB1 := netip.MustParsePrefix("10.2.0.0/24") for prefix, peer := range map[netip.Prefix]string{peerA1: "peerA", peerA2: "peerA", peerB1: "peerB"} { _, err := m.allowedIPsRefCounter.Increment(prefix, peer) require.NoError(t, err) } // Extra reference: reconcile must still re-apply the prefix even though its refcount never // hit 0 again (the exact case the plain incremental path skips). _, err := m.allowedIPsRefCounter.Increment(peerA1, "peerA") require.NoError(t, err) require.NoError(t, m.ReconcilePeerAllowedIPs("peerA")) assert.ElementsMatch(t, []netip.Prefix{peerA1, peerA2}, wg.added("peerA"), "reconcile must re-apply all routed prefixes of the peer") assert.Empty(t, wg.added("peerB"), "reconcile must not touch another peer's prefixes") } // TestReconcilePeerAllowedIPsNoCounter verifies reconcile is a safe no-op before the refcounter is // set up. func TestReconcilePeerAllowedIPsNoCounter(t *testing.T) { wg := &reconcileWGMock{} m := &DefaultManager{wgInterface: wg} require.NoError(t, m.ReconcilePeerAllowedIPs("peerA")) assert.Empty(t, wg.added("peerA")) }