mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-22 00:11:29 +02:00
When a peer went idle under lazy connections, the WireGuard peer was removed and the wake endpoint re-armed with only the overlay allowed IPs, dropping routed prefixes installed by the route manager. The asynchronous route-watcher re-add uses update_only, which is a silent no-op while the peer is absent, so routed subnets stayed black-holed until the peer was woken by other means. Split the idle transition from the full close: Conn.Idle tears down transports, cancels pending delayed endpoint updates and updates the status without touching the WireGuard peer. The activity listener then arms the wake endpoint via the new IdlePeerEndpoint operation, which removes and re-creates the peer in a single transaction: handshake state is dropped (keeping the handshake-first wake semantics of packet staging and first-packet capture/reinjection) while the currently installed allowed IPs are preserved. The read-modify-write runs under the interface mutex, serializing it against concurrent allowed IP updates from the route manager. Conn.Close no longer takes signalToRemote: the idle transition is the only path that signals GOAWAY to the remote peer.
1403 lines
37 KiB
Go
1403 lines
37 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
|
|
|
"github.com/netbirdio/netbird/client/iface"
|
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
|
"github.com/netbirdio/netbird/client/iface/device"
|
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
|
"github.com/netbirdio/netbird/client/internal/dns"
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
nbdns "github.com/netbirdio/netbird/dns"
|
|
"github.com/netbirdio/netbird/monotime"
|
|
"github.com/netbirdio/netbird/route"
|
|
mgmt "github.com/netbirdio/netbird/shared/management/client"
|
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/shared/netiputil"
|
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
type MockWGIface struct {
|
|
CreateFunc func() error
|
|
CreateOnAndroidFunc func(routeRange []string, ip string, domains []string) error
|
|
IsUserspaceBindFunc func() bool
|
|
NameFunc func() string
|
|
AddressFunc func() wgaddr.Address
|
|
ToInterfaceFunc func() *net.Interface
|
|
UpFunc func() (*udpmux.UniversalUDPMuxDefault, error)
|
|
UpdateAddrFunc func(newAddr wgaddr.Address) error
|
|
UpdatePeerFunc func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
|
|
RemovePeerFunc func(peerKey string) error
|
|
IdlePeerEndpointFunc func(peerKey string, allowedIPs []netip.Prefix, endpoint *net.UDPAddr) error
|
|
AddAllowedIPFunc func(peerKey string, allowedIP netip.Prefix) error
|
|
RemoveAllowedIPFunc func(peerKey string, allowedIP netip.Prefix) error
|
|
CloseFunc func() error
|
|
SetFilterFunc func(filter device.PacketFilter) error
|
|
GetFilterFunc func() device.PacketFilter
|
|
GetDeviceFunc func() *device.FilteredDevice
|
|
GetWGDeviceFunc func() *wgdevice.Device
|
|
GetStatsFunc func() (map[string]configurer.WGStats, error)
|
|
GetInterfaceGUIDStringFunc func() (string, error)
|
|
GetProxyFunc func() wgproxy.Proxy
|
|
GetProxyPortFunc func() uint16
|
|
GetNetFunc func() *netstack.Net
|
|
LastActivitiesFunc func() map[string]monotime.Time
|
|
}
|
|
|
|
func (m *MockWGIface) RenewTun(_ int) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockWGIface) RemoveEndpointAddress(_ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockWGIface) FullStats() (*configurer.Stats, error) {
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
|
|
return m.GetInterfaceGUIDStringFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Create() error {
|
|
return m.CreateFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error {
|
|
return m.CreateOnAndroidFunc(routeRange, ip, domains)
|
|
}
|
|
|
|
func (m *MockWGIface) IsUserspaceBind() bool {
|
|
return m.IsUserspaceBindFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Name() string {
|
|
return m.NameFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Address() wgaddr.Address {
|
|
return m.AddressFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) ToInterface() *net.Interface {
|
|
return m.ToInterfaceFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|
return m.UpFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) UpdateAddr(newAddr wgaddr.Address) error {
|
|
return m.UpdateAddrFunc(newAddr)
|
|
}
|
|
|
|
func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
|
|
return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
|
|
}
|
|
|
|
func (m *MockWGIface) RemovePeer(peerKey string) error {
|
|
return m.RemovePeerFunc(peerKey)
|
|
}
|
|
|
|
func (m *MockWGIface) IdlePeerEndpoint(peerKey string, allowedIPs []netip.Prefix, endpoint *net.UDPAddr) error {
|
|
if m.IdlePeerEndpointFunc == nil {
|
|
return nil
|
|
}
|
|
return m.IdlePeerEndpointFunc(peerKey, allowedIPs, endpoint)
|
|
}
|
|
|
|
func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
|
return m.AddAllowedIPFunc(peerKey, allowedIP)
|
|
}
|
|
|
|
func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
|
return m.RemoveAllowedIPFunc(peerKey, allowedIP)
|
|
}
|
|
|
|
func (m *MockWGIface) Close() error {
|
|
return m.CloseFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) SetFilter(filter device.PacketFilter) error {
|
|
return m.SetFilterFunc(filter)
|
|
}
|
|
|
|
func (m *MockWGIface) GetFilter() device.PacketFilter {
|
|
return m.GetFilterFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetDevice() *device.FilteredDevice {
|
|
return m.GetDeviceFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
|
|
return m.GetWGDeviceFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetStats() (map[string]configurer.WGStats, error) {
|
|
return m.GetStatsFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetProxy() wgproxy.Proxy {
|
|
return m.GetProxyFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) GetProxyPort() uint16 {
|
|
if m.GetProxyPortFunc != nil {
|
|
return m.GetProxyPortFunc()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m *MockWGIface) GetNet() *netstack.Net {
|
|
return m.GetNetFunc()
|
|
}
|
|
|
|
func (m *MockWGIface) LastActivities() map[string]monotime.Time {
|
|
if m.LastActivitiesFunc != nil {
|
|
return m.LastActivitiesFunc()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockWGIface) MTU() uint16 {
|
|
return 1280
|
|
}
|
|
|
|
func (m *MockWGIface) SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error {
|
|
return nil
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
_ = util.InitLog("debug", util.LogConsole)
|
|
code := m.Run()
|
|
os.Exit(code)
|
|
}
|
|
|
|
func TestEngine_SSHUpdateLogic(t *testing.T) {
|
|
// Test that SSH server start/stop logic works based on config
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
ServerSSHAllowed: false, // Start with SSH disabled
|
|
},
|
|
syncMsgMux: &sync.Mutex{},
|
|
}
|
|
|
|
// Test SSH disabled config
|
|
sshConfig := &mgmtProto.SSHConfig{SshEnabled: false}
|
|
err := engine.updateSSH(sshConfig)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
|
|
// Test inbound blocked
|
|
engine.config.BlockInbound = true
|
|
err = engine.updateSSH(&mgmtProto.SSHConfig{SshEnabled: true})
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
engine.config.BlockInbound = false
|
|
|
|
// Test with server SSH not allowed
|
|
err = engine.updateSSH(&mgmtProto.SSHConfig{SshEnabled: true})
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
}
|
|
|
|
func TestEngine_SSHServerConsistency(t *testing.T) {
|
|
|
|
t.Run("server set only on successful creation", func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
ServerSSHAllowed: true,
|
|
SSHKey: []byte("test-key"),
|
|
},
|
|
syncMsgMux: &sync.Mutex{},
|
|
}
|
|
|
|
engine.wgInterface = nil
|
|
|
|
err := engine.updateSSH(&mgmtProto.SSHConfig{SshEnabled: true})
|
|
|
|
assert.Error(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
})
|
|
|
|
t.Run("cleanup handles nil gracefully", func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
ServerSSHAllowed: false,
|
|
},
|
|
syncMsgMux: &sync.Mutex{},
|
|
}
|
|
|
|
err := engine.stopSSHServer()
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, engine.sshServer)
|
|
})
|
|
}
|
|
|
|
func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|
// test setup
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
|
defer cancel()
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: "utun102",
|
|
WgAddr: wgaddr.MustParseWGAddress("100.64.0.1/24"),
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
|
|
wgIface := &MockWGIface{
|
|
NameFunc: func() string { return "utun102" },
|
|
RemovePeerFunc: func(peerKey string) error {
|
|
return nil
|
|
},
|
|
AddressFunc: func() wgaddr.Address {
|
|
return wgaddr.Address{
|
|
IP: netip.MustParseAddr("10.20.0.1"),
|
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
|
}
|
|
},
|
|
UpdatePeerFunc: func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
|
|
return nil
|
|
},
|
|
}
|
|
engine.wgInterface = wgIface
|
|
engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{
|
|
Context: ctx,
|
|
PublicKey: key.PublicKey().String(),
|
|
DNSRouteInterval: time.Minute,
|
|
WGInterface: engine.wgInterface,
|
|
StatusRecorder: engine.statusRecorder,
|
|
RelayManager: relayMgr,
|
|
})
|
|
err = engine.routeManager.Init()
|
|
require.NoError(t, err)
|
|
engine.dnsServer = &dns.MockServer{
|
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
|
|
}
|
|
conn, err := net.ListenUDP("udp4", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
engine.udpMux = udpmux.NewUniversalUDPMuxDefault(udpmux.UniversalUDPMuxParams{UDPConn: conn, MTU: 1280})
|
|
engine.ctx = ctx
|
|
engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{})
|
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface)
|
|
engine.connMgr.Start(ctx)
|
|
|
|
type testCase struct {
|
|
name string
|
|
networkMap *mgmtProto.NetworkMap
|
|
|
|
expectedLen int
|
|
expectedPeers []*mgmtProto.RemotePeerConfig
|
|
expectedSerial uint64
|
|
}
|
|
|
|
peer1 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.10/24"},
|
|
}
|
|
|
|
peer2 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.11/24"},
|
|
}
|
|
|
|
peer3 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.12/24"},
|
|
}
|
|
|
|
modifiedPeer3 := &mgmtProto.RemotePeerConfig{
|
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
|
AllowedIps: []string{"100.64.0.20/24"},
|
|
}
|
|
|
|
case1 := testCase{
|
|
name: "input with a new peer to add",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer1,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 1,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1},
|
|
expectedSerial: 1,
|
|
}
|
|
|
|
// 2nd case - one extra peer added and network map has CurrentSerial grater than local => apply the update
|
|
case2 := testCase{
|
|
name: "input with an old peer and a new peer to add",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 2,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer1, peer2,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
|
expectedSerial: 2,
|
|
}
|
|
|
|
case3 := testCase{
|
|
name: "input with outdated (old) update to ignore",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 0,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer1, peer2, peer3,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
|
expectedSerial: 2,
|
|
}
|
|
|
|
case4 := testCase{
|
|
name: "input with one peer to remove and one new to add",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 4,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
peer2, peer3,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, peer3},
|
|
expectedSerial: 4,
|
|
}
|
|
|
|
case5 := testCase{
|
|
name: "input with one peer to modify",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 4,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
|
modifiedPeer3, peer2,
|
|
},
|
|
RemotePeersIsEmpty: false,
|
|
},
|
|
expectedLen: 2,
|
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, modifiedPeer3},
|
|
expectedSerial: 4,
|
|
}
|
|
|
|
case6 := testCase{
|
|
name: "input with all peers to remove",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 5,
|
|
PeerConfig: nil,
|
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
|
RemotePeersIsEmpty: true,
|
|
},
|
|
expectedLen: 0,
|
|
expectedPeers: nil,
|
|
expectedSerial: 5,
|
|
}
|
|
|
|
for _, c := range []testCase{case1, case2, case3, case4, case5, case6} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
err = engine.updateNetworkMap(c.networkMap)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
if len(engine.peerStore.PeersPubKey()) != c.expectedLen {
|
|
t.Errorf("expecting Engine.peerConns to be of size %d, got %d", c.expectedLen, len(engine.peerStore.PeersPubKey()))
|
|
}
|
|
|
|
if engine.networkSerial != c.expectedSerial {
|
|
t.Errorf("expecting Engine.networkSerial to be equal to %d, actual %d", c.expectedSerial, engine.networkSerial)
|
|
}
|
|
|
|
for _, p := range c.expectedPeers {
|
|
conn, ok := engine.peerStore.PeerConn(p.GetWgPubKey())
|
|
if !ok {
|
|
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
|
}
|
|
expectedAllowedIPs := strings.Join(p.AllowedIps, ",")
|
|
if !compareNetIPLists(conn.WgConfig().AllowedIps, p.AllowedIps) {
|
|
t.Errorf("expecting peer %s to have AllowedIPs= %s, got %s", p.GetWgPubKey(),
|
|
expectedAllowedIPs, conn.WgConfig().AllowedIps)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputErr error
|
|
networkMap *mgmtProto.NetworkMap
|
|
expectedLen int
|
|
expectedClientRoutes route.HAMap
|
|
expectedSerial uint64
|
|
}{
|
|
{
|
|
name: "Routes Config Should Be Passed To Manager",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: []*mgmtProto.Route{
|
|
{
|
|
ID: "a",
|
|
Network: "192.168.0.0/24",
|
|
NetID: "n1",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
{
|
|
ID: "b",
|
|
Network: "192.168.1.0/24",
|
|
NetID: "n2",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
},
|
|
},
|
|
expectedLen: 2,
|
|
expectedClientRoutes: route.HAMap{
|
|
"n1|192.168.0.0/24": []*route.Route{
|
|
{
|
|
ID: "a",
|
|
Network: netip.MustParsePrefix("192.168.0.0/24"),
|
|
NetID: "n1",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
},
|
|
"n2|192.168.1.0/24": []*route.Route{
|
|
{
|
|
ID: "b",
|
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
|
NetID: "n2",
|
|
Peer: "p1",
|
|
NetworkType: 1,
|
|
Masquerade: false,
|
|
},
|
|
},
|
|
},
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Empty Routes Config Should Be Passed",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
},
|
|
expectedLen: 0,
|
|
expectedClientRoutes: nil,
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Error Shouldn't Break Engine",
|
|
inputErr: fmt.Errorf("mocking error"),
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
},
|
|
expectedLen: 0,
|
|
expectedClientRoutes: nil,
|
|
expectedSerial: 1,
|
|
},
|
|
}
|
|
|
|
for n, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
// test setup
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
|
defer cancel()
|
|
|
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: wgIfaceName,
|
|
WgAddr: wgaddr.MustParseWGAddress(wgAddr),
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
engine.ctx = ctx
|
|
newNet, err := stdnet.NewNet(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
opts := iface.WGIFaceOpts{
|
|
IFaceName: wgIfaceName,
|
|
Address: wgaddr.MustParseWGAddress(wgAddr),
|
|
WGPort: engine.config.WgPort,
|
|
WGPrivKey: key.String(),
|
|
MTU: iface.DefaultMTU,
|
|
TransportNet: newNet,
|
|
}
|
|
engine.wgInterface, err = iface.NewWGIFace(opts)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
input := struct {
|
|
inputSerial uint64
|
|
clientRoutes route.HAMap
|
|
}{}
|
|
|
|
mockRouteManager := &routemanager.MockManager{
|
|
UpdateRoutesFunc: func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error {
|
|
input.inputSerial = updateSerial
|
|
input.clientRoutes = clientRoutes
|
|
return testCase.inputErr
|
|
},
|
|
ClassifyRoutesFunc: func(newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap) {
|
|
if len(newRoutes) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Classify all routes as client routes (not matching our public key)
|
|
clientRoutes := make(route.HAMap)
|
|
for _, r := range newRoutes {
|
|
haID := r.GetHAUniqueID()
|
|
clientRoutes[haID] = append(clientRoutes[haID], r)
|
|
}
|
|
return nil, clientRoutes
|
|
},
|
|
}
|
|
|
|
engine.routeManager = mockRouteManager
|
|
engine.dnsServer = &dns.MockServer{}
|
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface)
|
|
engine.connMgr.Start(ctx)
|
|
|
|
defer func() {
|
|
exitErr := engine.Stop()
|
|
if exitErr != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
err = engine.updateNetworkMap(testCase.networkMap)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
|
assert.Len(t, input.clientRoutes, testCase.expectedLen, "clientRoutes len should match")
|
|
assert.Equal(t, testCase.expectedClientRoutes, input.clientRoutes, "clientRoutes should match")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputErr error
|
|
networkMap *mgmtProto.NetworkMap
|
|
expectedZonesLen int
|
|
expectedZones []nbdns.CustomZone
|
|
expectedNSGroupsLen int
|
|
expectedNSGroups []*nbdns.NameServerGroup
|
|
expectedSerial uint64
|
|
}{
|
|
{
|
|
name: "DNS Config Should Be Passed To DNS Server",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
DNSConfig: &mgmtProto.DNSConfig{
|
|
ServiceEnable: true,
|
|
CustomZones: []*mgmtProto.CustomZone{
|
|
{
|
|
Domain: "netbird.cloud.",
|
|
Records: []*mgmtProto.SimpleRecord{
|
|
{
|
|
Name: "peer-a.netbird.cloud.",
|
|
Type: 1,
|
|
Class: nbdns.DefaultClass,
|
|
TTL: 300,
|
|
RData: "100.64.0.1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Domain: "0.66.100.in-addr.arpa.",
|
|
},
|
|
},
|
|
NameServerGroups: []*mgmtProto.NameServerGroup{
|
|
{
|
|
Primary: true,
|
|
NameServers: []*mgmtProto.NameServer{
|
|
{
|
|
IP: "8.8.8.8",
|
|
NSType: 1,
|
|
Port: 53,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedZonesLen: 1,
|
|
expectedZones: []nbdns.CustomZone{
|
|
{
|
|
Domain: "netbird.cloud.",
|
|
Records: []nbdns.SimpleRecord{
|
|
{
|
|
Name: "peer-a.netbird.cloud.",
|
|
Type: 1,
|
|
Class: nbdns.DefaultClass,
|
|
TTL: 300,
|
|
RData: "100.64.0.1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Domain: "0.66.100.in-addr.arpa.",
|
|
},
|
|
},
|
|
expectedNSGroupsLen: 1,
|
|
expectedNSGroups: []*nbdns.NameServerGroup{
|
|
{
|
|
Primary: true,
|
|
NameServers: []nbdns.NameServer{
|
|
{
|
|
IP: netip.MustParseAddr("8.8.8.8"),
|
|
NSType: 1,
|
|
Port: 53,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Empty DNS Config Should Be OK",
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
DNSConfig: nil,
|
|
},
|
|
expectedZonesLen: 0,
|
|
expectedZones: []nbdns.CustomZone{},
|
|
expectedNSGroupsLen: 0,
|
|
expectedNSGroups: []*nbdns.NameServerGroup{},
|
|
expectedSerial: 1,
|
|
},
|
|
{
|
|
name: "Error Shouldn't Break Engine",
|
|
inputErr: fmt.Errorf("mocking error"),
|
|
networkMap: &mgmtProto.NetworkMap{
|
|
Serial: 1,
|
|
PeerConfig: nil,
|
|
RemotePeersIsEmpty: false,
|
|
Routes: nil,
|
|
},
|
|
expectedZonesLen: 0,
|
|
expectedZones: []nbdns.CustomZone{},
|
|
expectedNSGroupsLen: 0,
|
|
expectedNSGroups: []*nbdns.NameServerGroup{},
|
|
expectedSerial: 1,
|
|
},
|
|
}
|
|
|
|
for n, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
// test setup
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
|
defer cancel()
|
|
|
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
|
|
|
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
|
|
engine := NewEngine(ctx, cancel, &EngineConfig{
|
|
WgIfaceName: wgIfaceName,
|
|
WgAddr: wgaddr.MustParseWGAddress(wgAddr),
|
|
WgPrivateKey: key,
|
|
WgPort: 33100,
|
|
MTU: iface.DefaultMTU,
|
|
}, EngineServices{
|
|
SignalClient: &signal.MockClient{},
|
|
MgmClient: &mgmt.MockClient{},
|
|
RelayManager: relayMgr,
|
|
StatusRecorder: peer.NewRecorder("https://mgm"),
|
|
}, MobileDependency{})
|
|
engine.ctx = ctx
|
|
|
|
newNet, err := stdnet.NewNet(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := iface.WGIFaceOpts{
|
|
IFaceName: wgIfaceName,
|
|
Address: wgaddr.MustParseWGAddress(wgAddr),
|
|
WGPort: 33100,
|
|
WGPrivKey: key.String(),
|
|
MTU: iface.DefaultMTU,
|
|
TransportNet: newNet,
|
|
}
|
|
engine.wgInterface, err = iface.NewWGIFace(opts)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
|
|
mockRouteManager := &routemanager.MockManager{
|
|
UpdateRoutesFunc: func(updateSerial uint64, serverRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
engine.routeManager = mockRouteManager
|
|
|
|
input := struct {
|
|
inputSerial uint64
|
|
inputNSGroups []*nbdns.NameServerGroup
|
|
inputZones []nbdns.CustomZone
|
|
}{}
|
|
|
|
mockDNSServer := &dns.MockServer{
|
|
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error {
|
|
input.inputSerial = serial
|
|
input.inputZones = update.CustomZones
|
|
input.inputNSGroups = update.NameServerGroups
|
|
return testCase.inputErr
|
|
},
|
|
}
|
|
|
|
engine.dnsServer = mockDNSServer
|
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface)
|
|
engine.connMgr.Start(ctx)
|
|
|
|
defer func() {
|
|
exitErr := engine.Stop()
|
|
if exitErr != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
err = engine.updateNetworkMap(testCase.networkMap)
|
|
assert.NoError(t, err, "shouldn't return error")
|
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
|
assert.Len(t, input.inputNSGroups, testCase.expectedZonesLen, "zones len should match")
|
|
assert.Equal(t, testCase.expectedZones, input.inputZones, "custom zones should match")
|
|
assert.Len(t, input.inputNSGroups, testCase.expectedNSGroupsLen, "ns groups len should match")
|
|
assert.Equal(t, testCase.expectedNSGroups, input.inputNSGroups, "ns groups should match")
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ParseNATExternalIPMappings(t *testing.T) {
|
|
ifaceList, err := net.Interfaces()
|
|
if err != nil {
|
|
t.Fatalf("could get the interface list, got error: %s", err)
|
|
}
|
|
|
|
var testingIP string
|
|
var testingInterface string
|
|
|
|
for _, iface := range ifaceList {
|
|
addrList, err := iface.Addrs()
|
|
if err != nil {
|
|
t.Fatalf("could get the addr list, got error: %s", err)
|
|
}
|
|
for _, addr := range addrList {
|
|
prefix := netip.MustParsePrefix(addr.String())
|
|
if prefix.Addr().Is4() && !prefix.Addr().IsLoopback() {
|
|
testingIP = prefix.Addr().String()
|
|
testingInterface = iface.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
inputMapList []string
|
|
inputBlacklistInterface []string
|
|
expectedOutput []string
|
|
}{
|
|
{
|
|
name: "Parse Valid List Should Be OK",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{"1.1.1.1", "8.8.8.8/" + testingInterface},
|
|
expectedOutput: []string{"1.1.1.1", "8.8.8.8/" + testingIP},
|
|
},
|
|
{
|
|
name: "Only Interface name Should Return Nil",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{testingInterface},
|
|
expectedOutput: nil,
|
|
},
|
|
{
|
|
name: "Invalid IP Return Nil",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{"1.1.1.1000"},
|
|
expectedOutput: nil,
|
|
},
|
|
{
|
|
name: "Invalid Mapping Element Should return Nil",
|
|
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
|
|
inputMapList: []string{"1.1.1.1/10.10.10.1/eth0"},
|
|
expectedOutput: nil,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{
|
|
IFaceBlackList: testCase.inputBlacklistInterface,
|
|
NATExternalIPs: testCase.inputMapList,
|
|
MTU: iface.DefaultMTU,
|
|
},
|
|
}
|
|
parsedList := engine.parseNATExternalIPMappings()
|
|
require.ElementsMatchf(t, testCase.expectedOutput, parsedList, "elements of parsed list should match expected list")
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_CheckFilesEqual(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputChecks1 []*mgmtProto.Checks
|
|
inputChecks2 []*mgmtProto.Checks
|
|
expectedBool bool
|
|
}{
|
|
{
|
|
name: "Equal Files In Equal Order Should Return True",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
{
|
|
name: "Equal Files In Reverse Order Should Return True",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile2",
|
|
"testfile1",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
{
|
|
name: "Unequal Files Should Return False",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile3",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: false,
|
|
},
|
|
{
|
|
name: "Compared With Empty Should Return False",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{},
|
|
},
|
|
},
|
|
expectedBool: false,
|
|
},
|
|
{
|
|
name: "Compared Slices with same files but different order should return true",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile1",
|
|
"testfile2",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile4",
|
|
"testfile3",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile3",
|
|
"testfile4",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile2",
|
|
"testfile1",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
{
|
|
name: "Compared Slices with same files but different order while first is equal should return true",
|
|
inputChecks1: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile1",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile2",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile3",
|
|
},
|
|
},
|
|
},
|
|
inputChecks2: []*mgmtProto.Checks{
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile1",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile3",
|
|
},
|
|
},
|
|
{
|
|
Files: []string{
|
|
"testfile0",
|
|
"testfile2",
|
|
},
|
|
},
|
|
},
|
|
expectedBool: true,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
result := isChecksEqual(testCase.inputChecks1, testCase.inputChecks2)
|
|
assert.Equal(t, testCase.expectedBool, result, "result should match expected bool")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareNetIPLists(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
list1 []netip.Prefix
|
|
list2 []string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "both empty",
|
|
list1: []netip.Prefix{},
|
|
list2: []string{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "single match ipv4",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")},
|
|
list2: []string{"192.168.0.0/24"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "multiple match ipv4, different order",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24"), netip.MustParsePrefix("10.0.0.0/8")},
|
|
list2: []string{"10.0.0.0/8", "192.168.1.0/24"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ipv4 mismatch due to extra element in list2",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"192.168.1.0/24", "10.0.0.0/8"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ipv4 mismatch due to duplicate count",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24"), netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"192.168.1.0/24"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "invalid prefix in list2",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"invalid-prefix"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ipv4 mismatch because different prefixes",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")},
|
|
list2: []string{"10.0.0.0/8"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "single match ipv6",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("2001:db8::/32")},
|
|
list2: []string{"2001:db8::/32"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "multiple match ipv6, different order",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("2001:db8::/32"), netip.MustParsePrefix("fe80::/10")},
|
|
list2: []string{"fe80::/10", "2001:db8::/32"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "mixed ipv4 and ipv6 match",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24"), netip.MustParsePrefix("2001:db8::/32")},
|
|
list2: []string{"2001:db8::/32", "192.168.1.0/24"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ipv6 mismatch with invalid prefix",
|
|
list1: []netip.Prefix{netip.MustParsePrefix("2001:db8::/32")},
|
|
list2: []string{"invalid-ipv6"},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := compareNetIPLists(tt.list1, tt.list2)
|
|
if result != tt.expected {
|
|
t.Errorf("compareNetIPLists(%v, %v) = %v; want %v", tt.list1, tt.list2, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustEncodePrefix(t *testing.T, p netip.Prefix) []byte {
|
|
t.Helper()
|
|
b, err := netiputil.EncodePrefix(p)
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
func TestEngine_hasIPv6Changed(t *testing.T) {
|
|
v4Only := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
|
|
|
v4v6 := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
|
v4v6.IPv6 = netip.MustParseAddr("fd00::1")
|
|
v4v6.IPv6Net = netip.MustParsePrefix("fd00::1/64").Masked()
|
|
|
|
tests := []struct {
|
|
name string
|
|
current wgaddr.Address
|
|
confV6 []byte
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "no v6 before, no v6 now",
|
|
current: v4Only,
|
|
confV6: nil,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "no v6 before, v6 added",
|
|
current: v4Only,
|
|
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::1/64")),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "had v6, now removed",
|
|
current: v4v6,
|
|
confV6: nil,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "had v6, same v6",
|
|
current: v4v6,
|
|
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::1/64")),
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "had v6, different v6",
|
|
current: v4v6,
|
|
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::2/64")),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "same v6 addr, different prefix length",
|
|
current: v4v6,
|
|
confV6: mustEncodePrefix(t, netip.MustParsePrefix("fd00::1/80")),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "decode error keeps status quo",
|
|
current: v4Only,
|
|
confV6: []byte{1, 2, 3},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
engine := &Engine{
|
|
config: &EngineConfig{WgAddr: tt.current},
|
|
}
|
|
conf := &mgmtProto.PeerConfig{
|
|
AddressV6: tt.confV6,
|
|
}
|
|
assert.Equal(t, tt.expected, engine.hasIPv6Changed(conf))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterAllowedIPs(t *testing.T) {
|
|
v4v6Addr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
|
v4v6Addr.IPv6 = netip.MustParseAddr("fd00::1")
|
|
v4v6Addr.IPv6Net = netip.MustParsePrefix("fd00::1/64").Masked()
|
|
|
|
v4OnlyAddr := wgaddr.MustParseWGAddress("100.64.0.1/16")
|
|
|
|
tests := []struct {
|
|
name string
|
|
addr wgaddr.Address
|
|
input []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "interface has v6, keep all",
|
|
addr: v4v6Addr,
|
|
input: []string{"100.64.0.1/32", "fd00::1/128"},
|
|
expected: []string{"100.64.0.1/32", "fd00::1/128"},
|
|
},
|
|
{
|
|
name: "no v6, strip v6",
|
|
addr: v4OnlyAddr,
|
|
input: []string{"100.64.0.1/32", "fd00::1/128"},
|
|
expected: []string{"100.64.0.1/32"},
|
|
},
|
|
{
|
|
name: "no v6, only v4",
|
|
addr: v4OnlyAddr,
|
|
input: []string{"100.64.0.1/32", "10.0.0.0/8"},
|
|
expected: []string{"100.64.0.1/32", "10.0.0.0/8"},
|
|
},
|
|
{
|
|
name: "no v6, only v6 input",
|
|
addr: v4OnlyAddr,
|
|
input: []string{"fd00::1/128", "::/0"},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "no v6, invalid prefix preserved",
|
|
addr: v4OnlyAddr,
|
|
input: []string{"100.64.0.1/32", "garbage"},
|
|
expected: []string{"100.64.0.1/32", "garbage"},
|
|
},
|
|
{
|
|
name: "no v6, empty input",
|
|
addr: v4OnlyAddr,
|
|
input: []string{},
|
|
expected: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
addr := tt.addr
|
|
engine := &Engine{
|
|
config: &EngineConfig{},
|
|
wgInterface: &MockWGIface{
|
|
AddressFunc: func() wgaddr.Address { return addr },
|
|
},
|
|
}
|
|
result := engine.filterAllowedIPs(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOverlayAddrsFromAllowedIPs(t *testing.T) {
|
|
ourV6Net := netip.MustParsePrefix("fd00:1234:5678:abcd::/64")
|
|
|
|
tests := []struct {
|
|
name string
|
|
allowedIPs []string
|
|
ourV6Net netip.Prefix
|
|
wantV4 string
|
|
wantV6 string
|
|
}{
|
|
{
|
|
name: "v4 only",
|
|
allowedIPs: []string{"100.64.0.1/32"},
|
|
ourV6Net: ourV6Net,
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "",
|
|
},
|
|
{
|
|
name: "v4 and v6 overlay",
|
|
allowedIPs: []string{"100.64.0.1/32", "fd00:1234:5678:abcd::1/128"},
|
|
ourV6Net: ourV6Net,
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "fd00:1234:5678:abcd::1",
|
|
},
|
|
{
|
|
name: "v4, routed v6, overlay v6",
|
|
allowedIPs: []string{"100.64.0.1/32", "2001:db8::1/128", "fd00:1234:5678:abcd::1/128"},
|
|
ourV6Net: ourV6Net,
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "fd00:1234:5678:abcd::1",
|
|
},
|
|
{
|
|
name: "routed v6 /128 outside our subnet is ignored",
|
|
allowedIPs: []string{"100.64.0.1/32", "2001:db8::1/128"},
|
|
ourV6Net: ourV6Net,
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "",
|
|
},
|
|
{
|
|
name: "routed v6 prefix is ignored",
|
|
allowedIPs: []string{"100.64.0.1/32", "fd00:1234:5678:abcd::/64"},
|
|
ourV6Net: ourV6Net,
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "",
|
|
},
|
|
{
|
|
name: "no v6 subnet configured",
|
|
allowedIPs: []string{"100.64.0.1/32", "fd00:1234:5678:abcd::1/128"},
|
|
ourV6Net: netip.Prefix{},
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "",
|
|
},
|
|
{
|
|
name: "v4 /24 route is ignored",
|
|
allowedIPs: []string{"100.64.0.0/24", "100.64.0.1/32"},
|
|
ourV6Net: ourV6Net,
|
|
wantV4: "100.64.0.1",
|
|
wantV6: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
v4, v6 := overlayAddrsFromAllowedIPs(tt.allowedIPs, tt.ourV6Net)
|
|
if tt.wantV4 == "" {
|
|
assert.False(t, v4.IsValid(), "expected no v4")
|
|
} else {
|
|
assert.Equal(t, tt.wantV4, v4.String(), "v4")
|
|
}
|
|
if tt.wantV6 == "" {
|
|
assert.False(t, v6.IsValid(), "expected no v6")
|
|
} else {
|
|
assert.Equal(t, tt.wantV6, v6.String(), "v6")
|
|
}
|
|
})
|
|
}
|
|
}
|