150 lines
4.4 KiB
Go
150 lines
4.4 KiB
Go
package ipam
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"remlink/internal/database"
|
|
"remlink/internal/model"
|
|
)
|
|
|
|
func TestReserveTenNodesUniqueStableAndConcurrent(t *testing.T) {
|
|
manager, store := testManager(t, "10.88.0.0/16", "10.88.0.1")
|
|
ctx := context.Background()
|
|
const count = 10
|
|
results := make(chan model.Node, count)
|
|
errorsChannel := make(chan error, count)
|
|
var group sync.WaitGroup
|
|
for index := range count {
|
|
group.Add(1)
|
|
go func() {
|
|
defer group.Done()
|
|
node, created, err := manager.ReserveNode(ctx, newNode(index))
|
|
if err != nil {
|
|
errorsChannel <- err
|
|
return
|
|
}
|
|
if !created {
|
|
errorsChannel <- fmt.Errorf("node %d was not newly created", index)
|
|
return
|
|
}
|
|
results <- node
|
|
}()
|
|
}
|
|
group.Wait()
|
|
close(results)
|
|
close(errorsChannel)
|
|
for err := range errorsChannel {
|
|
t.Error(err)
|
|
}
|
|
seen := make(map[netip.Addr]struct{}, count)
|
|
for result := range results {
|
|
if result.OverlayIP == netip.MustParseAddr("10.88.0.1") {
|
|
t.Fatal("Server address allocated to node")
|
|
}
|
|
if _, duplicate := seen[result.OverlayIP]; duplicate {
|
|
t.Fatalf("duplicate allocation %s", result.OverlayIP)
|
|
}
|
|
seen[result.OverlayIP] = struct{}{}
|
|
}
|
|
if len(seen) != count {
|
|
t.Fatalf("unique allocation count = %d, want %d", len(seen), count)
|
|
}
|
|
|
|
existing, err := store.GetNode(ctx, newNode(3).ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted, created, err := manager.ReserveNode(ctx, newNode(3))
|
|
if err != nil || created || restarted.OverlayIP != existing.OverlayIP {
|
|
t.Fatalf("restart allocation = %v, created=%v, err=%v; want %s", restarted.OverlayIP, created, err, existing.OverlayIP)
|
|
}
|
|
}
|
|
|
|
func TestManualChangeAndRelease(t *testing.T) {
|
|
manager, store := testManager(t, "10.88.0.0/24", "10.88.0.1")
|
|
ctx := context.Background()
|
|
first, _, err := manager.ReserveNode(ctx, newNode(1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, _, err := manager.ReserveNode(ctx, newNode(2))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := manager.ChangeNodeAddress(ctx, first.ID, second.OverlayIP); !errors.Is(err, ErrAddressUnavailable) {
|
|
t.Fatalf("occupied address error = %v", err)
|
|
}
|
|
for _, reserved := range []string{"10.88.0.0", "10.88.0.1", "10.88.0.255", "10.89.0.2"} {
|
|
if err := manager.ChangeNodeAddress(ctx, first.ID, netip.MustParseAddr(reserved)); !errors.Is(err, ErrAddressUnavailable) {
|
|
t.Errorf("reserved address %s error = %v", reserved, err)
|
|
}
|
|
}
|
|
desired := netip.MustParseAddr("10.88.0.100")
|
|
if err := manager.ChangeNodeAddress(ctx, first.ID, desired); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
updated, err := store.GetNode(ctx, first.ID)
|
|
if err != nil || updated.OverlayIP != desired {
|
|
t.Fatalf("updated address = %s, err=%v", updated.OverlayIP, err)
|
|
}
|
|
if err := manager.ReleaseNode(ctx, first.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.GetNode(ctx, first.ID); !errors.Is(err, database.ErrNodeNotFound) {
|
|
t.Fatalf("released node error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPoolExhaustion(t *testing.T) {
|
|
manager, _ := testManager(t, "10.88.0.0/30", "10.88.0.1")
|
|
ctx := context.Background()
|
|
if _, _, err := manager.ReserveNode(ctx, newNode(1)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := manager.ReserveNode(ctx, newNode(2)); !errors.Is(err, ErrAddressPoolExhausted) {
|
|
t.Fatalf("second /30 allocation error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIPAMRejectsExitNodePool(t *testing.T) {
|
|
db, err := database.Open(context.Background(), filepath.Join(t.TempDir(), "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
if _, err := New(database.NewStore(db), netip.MustParsePrefix("0.0.0.0/0"), netip.MustParseAddr("10.88.0.1")); err == nil {
|
|
t.Fatal("IPAM accepted 0.0.0.0/0 Exit Node pool")
|
|
}
|
|
}
|
|
|
|
func testManager(t *testing.T, prefix, server string) (*Manager, *database.Store) {
|
|
t.Helper()
|
|
db, err := database.Open(context.Background(), filepath.Join(t.TempDir(), "remlink.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
store := database.NewStore(db)
|
|
manager, err := New(store, netip.MustParsePrefix(prefix), netip.MustParseAddr(server))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return manager, store
|
|
}
|
|
|
|
func newNode(index int) model.Node {
|
|
return model.Node{
|
|
ID: fmt.Sprintf("00000000-0000-4000-8000-%012d", index),
|
|
Type: model.NodeTypeEngineer,
|
|
Name: fmt.Sprintf("node-%d", index),
|
|
WGPublicKey: fmt.Sprintf("public-key-%d", index),
|
|
NodeTokenHash: []byte(fmt.Sprintf("token-hash-%d", index)),
|
|
}
|
|
}
|