package admin import ( "context" "errors" "net/netip" "path/filepath" "testing" "time" "remlink/internal/bootstrap" "remlink/internal/database" "remlink/internal/ipam" "remlink/internal/model" "remlink/internal/overlay/serverwg" "remlink/internal/protocol" ) func TestNetworkManagerRunsSevenStepMigration(t *testing.T) { ctx := context.Background() db, err := database.Open(ctx, filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } defer db.Close() store := database.NewStore(db) for _, node := range []model.Node{ {ID: "engineer", Type: model.NodeTypeEngineer, Name: "Engineer", OverlayIP: netip.MustParseAddr("10.88.0.2"), WGPublicKey: "key-a", NodeTokenHash: []byte("a")}, {ID: "site", Type: model.NodeTypeSite, Name: "Site", OverlayIP: netip.MustParseAddr("10.88.0.3"), WGPublicKey: "key-b", NodeTokenHash: []byte("b")}, } { if err := store.CreateNode(ctx, node); err != nil { t.Fatal(err) } } ipamManager, _ := ipam.New(store, netip.MustParsePrefix("10.88.0.0/24"), netip.MustParseAddr("10.88.0.1")) steps := make([]string, 0, 4) peers := &fakeAdminPeers{steps: &steps} bootstrapService := &fakeBootstrapNetwork{config: bootstrap.ServiceConfig{ WGEndpoint: "203.0.113.4:51820", ControlURL: "ws://10.88.0.1:7001/control", OverlayCIDR: netip.MustParsePrefix("10.88.0.0/24"), ServerOverlayIP: netip.MustParseAddr("10.88.0.1"), ConfigVersion: 1, }} control := &fakeAdminControl{steps: &steps} sessions := &fakeSessionControl{} var rebound netip.Addr manager, err := NewNetworkManager(store, ipamManager, peers, bootstrapService, control, sessions, Network{ OverlayCIDR: "10.88.0.0/24", ServerOverlayIP: "10.88.0.1", WireGuardPort: 51820, SessionUDPPort: 6200, MTU: 1280, ConfigVersion: 1, }, func(address netip.Addr) error { steps = append(steps, "rebind"); rebound = address; return nil }) if err != nil { t.Fatal(err) } updated, err := manager.Update(ctx, NetworkUpdate{ OverlayCIDR: "10.99.0.0/24", ServerOverlayIP: "10.99.0.1", WireGuardPort: 51830, SessionUDPPort: 6300, MTU: 1400, }) if err != nil { t.Fatal(err) } if updated.ConfigVersion != 2 || !sessions.began || !sessions.ended || peers.address.String() != "10.99.0.1/24" || peers.port != 51830 { t.Fatalf("migration state updated=%+v sessions=%+v peers=%s:%d", updated, sessions, peers.address, peers.port) } if sessions.prefix.String() != "10.99.0.0/24" || sessions.mtu != 1400 || sessions.udpPort != 6300 { t.Fatalf("Session Manager network remained stale: prefix=%s mtu=%d udp=%d", sessions.prefix, sessions.mtu, sessions.udpPort) } engineer, _ := store.GetNode(ctx, "engineer") site, _ := store.GetNode(ctx, "site") if engineer.OverlayIP.String() != "10.99.0.2" || site.OverlayIP.String() != "10.99.0.3" { t.Fatalf("migrated addresses Engineer=%s Site=%s", engineer.OverlayIP, site.OverlayIP) } if rebound.String() != "10.99.0.1" || control.version != 2 || control.notifications != 2 || !control.resetAll { t.Fatalf("Control migration rebound=%s version=%d notifications=%d reset=%v", rebound, control.version, control.notifications, control.resetAll) } if bootstrapService.config.ControlURL != "ws://10.99.0.1:7001/control" || bootstrapService.config.WGEndpoint != "203.0.113.4:51830" { t.Fatalf("Bootstrap config = %+v", bootstrapService.config) } wantSteps := []string{"notify", "notify", "peers", "rebind"} if len(steps) != len(wantSteps) { t.Fatalf("migration steps = %v, want %v", steps, wantSteps) } for index := range wantSteps { if steps[index] != wantSteps[index] { t.Fatalf("migration steps = %v, want %v", steps, wantSteps) } } loaded, err := LoadStoredNetwork(ctx, store, Network{}) if err != nil || loaded != updated { t.Fatalf("stored network = %+v, %v", loaded, err) } } func TestNetworkManagerRollsBackKernelWhenControlRebindFails(t *testing.T) { ctx := context.Background() db, err := database.Open(ctx, filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } defer db.Close() store := database.NewStore(db) ipamManager, _ := ipam.New(store, netip.MustParsePrefix("10.88.0.0/24"), netip.MustParseAddr("10.88.0.1")) peers := &fakeAdminPeers{} bootstrapService := &fakeBootstrapNetwork{config: bootstrap.ServiceConfig{ WGEndpoint: "203.0.113.4:51820", ControlURL: "ws://10.88.0.1:7001/control", OverlayCIDR: netip.MustParsePrefix("10.88.0.0/24"), ServerOverlayIP: netip.MustParseAddr("10.88.0.1"), ConfigVersion: 1, }} sessions := &fakeSessionControl{} manager, err := NewNetworkManager(store, ipamManager, peers, bootstrapService, &fakeAdminControl{}, sessions, Network{ OverlayCIDR: "10.88.0.0/24", ServerOverlayIP: "10.88.0.1", WireGuardPort: 51820, SessionUDPPort: 6200, MTU: 1280, ConfigVersion: 1, }, func(netip.Addr) error { return errors.New("address unavailable") }) if err != nil { t.Fatal(err) } if _, err := manager.Update(ctx, NetworkUpdate{ OverlayCIDR: "10.99.0.0/24", ServerOverlayIP: "10.99.0.1", WireGuardPort: 51830, SessionUDPPort: 6300, MTU: 1400, }); err == nil { t.Fatal("network migration unexpectedly succeeded despite Control rebind failure") } if current := manager.Current(); current.ConfigVersion != 1 || current.OverlayCIDR != "10.88.0.0/24" { t.Fatalf("failed migration changed current config: %+v", current) } if peers.address.String() != "10.88.0.1/24" || peers.port != 51820 { t.Fatalf("failed migration left kernel config at %s:%d", peers.address, peers.port) } if sessions.prefix.String() != "10.88.0.0/24" || sessions.mtu != 1280 || sessions.udpPort != 6200 { t.Fatalf("failed migration did not restore Session Manager: %s mtu=%d udp=%d", sessions.prefix, sessions.mtu, sessions.udpPort) } if bootstrapService.config.OverlayCIDR.String() != "10.88.0.0/24" || bootstrapService.config.ConfigVersion != 1 { t.Fatalf("failed migration did not restore Bootstrap config: %+v", bootstrapService.config) } stored, err := LoadStoredNetwork(ctx, store, Network{}) if err != nil || stored != manager.Current() { t.Fatalf("failed migration did not restore atomic database setting: stored=%+v current=%+v err=%v", stored, manager.Current(), err) } } func TestAdminNetworkRejectsExitNodeOverlay(t *testing.T) { _, _, err := validateNetwork(Network{ OverlayCIDR: "0.0.0.0/0", ServerOverlayIP: "10.88.0.1", WireGuardPort: 51820, SessionUDPPort: 6200, MTU: 1280, ConfigVersion: 1, }) if err == nil { t.Fatal("Admin network accepted 0.0.0.0/0 Exit Node Overlay") } } type fakeAdminPeers struct { address netip.Prefix port int peers []serverwg.Peer steps *[]string ensured netip.Addr removed int } func (f *fakeAdminPeers) EnsurePeer(_ context.Context, _ string, address netip.Addr) error { if f.steps != nil { *f.steps = append(*f.steps, "peers") } f.ensured = address return nil } func (f *fakeAdminPeers) RemovePeer(context.Context, string) error { f.removed++ return nil } func (*fakeAdminPeers) LastHandshake(context.Context, string) (*time.Time, error) { value := time.Date(2026, 8, 25, 1, 2, 3, 0, time.UTC) return &value, nil } func (f *fakeAdminPeers) Reconfigure(_ context.Context, address netip.Prefix, port int, peers []serverwg.Peer) error { if f.steps != nil { *f.steps = append(*f.steps, "peers") } f.address, f.port, f.peers = address, port, peers return nil } type fakeBootstrapNetwork struct{ config bootstrap.ServiceConfig } func (f *fakeBootstrapNetwork) NetworkSnapshot() bootstrap.ServiceConfig { return f.config } func (f *fakeBootstrapNetwork) UpdateNetwork(config bootstrap.ServiceConfig) error { f.config = config return nil } type fakeAdminControl struct { version uint64 notifications int resetAll bool steps *[]string resetNode bool resetNodeCount int resetReason string } func (f *fakeAdminControl) Send(_ context.Context, _ string, messageType protocol.ControlMessageType, _ any) error { if messageType == protocol.ControlRebootstrapRequired { f.notifications++ if f.steps != nil { *f.steps = append(*f.steps, "notify") } } return nil } func (f *fakeAdminControl) SetNetworkConfigVersion(version uint64) error { f.version = version return nil } func (f *fakeAdminControl) ResetNodeConnection(_, reason string) { f.resetNode, f.resetReason = true, reason f.resetNodeCount++ } func (f *fakeAdminControl) ResetConnections(string) { f.resetAll = true } type fakeSessionControl struct { all bool began bool ended bool prefix netip.Prefix mtu int udpPort int nodeDisconnects int nodeReason string } func (*fakeSessionControl) Disconnect(context.Context, uint64, string) error { return nil } func (f *fakeSessionControl) DisconnectAll(context.Context, string) error { f.all = true; return nil } func (f *fakeSessionControl) DisconnectNode(_ context.Context, _, reason string) error { f.nodeDisconnects++ f.nodeReason = reason return nil } func (f *fakeSessionControl) ReconfigureNetwork(prefix netip.Prefix, mtu, udpPort int) error { f.prefix, f.mtu, f.udpPort = prefix, mtu, udpPort return nil } func (f *fakeSessionControl) BeginNetworkMigration(context.Context, string) error { f.began = true return nil } func (f *fakeSessionControl) EndNetworkMigration() { f.ended = true }