388 lines
15 KiB
Go
388 lines
15 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/netip"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"remlink/internal/overlay/clientwg"
|
|
"remlink/internal/platform/windows/route"
|
|
"remlink/internal/protocol"
|
|
"remlink/internal/subnetgateway"
|
|
)
|
|
|
|
func TestEngineerRuntimePreflightConfigureActivateStop(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.1")
|
|
routes := &fakeRoutes{}
|
|
device := &fakePacketDevice{}
|
|
control := &fakeNodeControl{}
|
|
runtime, err := NewEngineerRuntime(context.Background(), EngineerConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.1"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"),
|
|
UDPPort: port, StatsInterval: time.Hour, Routes: routes, Device: device, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
requestID, err := runtime.CreateSession(context.Background(), "site", []string{"192.168.13.0/24"})
|
|
if err != nil || requestID == "" || control.last(t, protocol.ControlCreateSession).requestID != requestID {
|
|
t.Fatalf("CreateSession = %q, %v", requestID, err)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlSessionConfig, requestID, protocol.SessionConfigPayload{
|
|
SessionID: 42, PeerOverlayIP: "127.0.0.2", CIDRs: []string{"192.168.13.0/24"}, MTU: 1280, UDPPort: port,
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(routes.added) != 1 || device.router == nil || control.last(t, protocol.ControlRoutesReady).payload.(protocol.RoutesReadyPayload).SessionID != 42 {
|
|
t.Fatalf("Engineer READY wiring routes=%v router=%v", routes.added, device.router)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlSessionActive, "", protocol.SessionActivePayload{SessionID: 42})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.Disconnect(context.Background(), "operator"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := control.last(t, protocol.ControlStopSession).payload.(protocol.StopSessionPayload); got.SessionID != 42 {
|
|
t.Fatalf("Disconnect payload = %+v", got)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlStopSession, "", protocol.StopSessionPayload{SessionID: 42, Reason: "operator"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if device.router != nil || len(routes.removed) != 1 {
|
|
t.Fatalf("Engineer cleanup router=%v removed=%v", device.router, routes.removed)
|
|
}
|
|
}
|
|
|
|
func TestEngineerRuntimeRejectsLocalConflictBeforeControl(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.1")
|
|
routes := &fakeRoutes{conflict: true}
|
|
control := &fakeNodeControl{}
|
|
runtime, err := NewEngineerRuntime(context.Background(), EngineerConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.1"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"),
|
|
UDPPort: port, Routes: routes, Device: &fakePacketDevice{}, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
if err := runtime.PreflightCIDRs([]string{"192.168.13.0/24"}); err == nil {
|
|
t.Fatal("preflight accepted local route conflict")
|
|
}
|
|
if control.count(protocol.ControlCreateSession) != 0 {
|
|
t.Fatal("preflight changed Control state")
|
|
}
|
|
if _, err := runtime.CreateSession(context.Background(), "site", []string{"192.168.13.0/24"}); err == nil {
|
|
t.Fatal("local route conflict was accepted")
|
|
}
|
|
if control.count(protocol.ControlCreateSession) != 0 {
|
|
t.Fatal("CREATE_SESSION was sent despite local conflict")
|
|
}
|
|
}
|
|
|
|
func TestEngineerRuntimePrepareFailureReleasesOnlyMatchingPendingRequest(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.1")
|
|
control := &fakeNodeControl{}
|
|
runtime, err := NewEngineerRuntime(context.Background(), EngineerConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.1"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"),
|
|
UDPPort: port, Routes: &fakeRoutes{}, Device: &fakePacketDevice{}, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
requestID, err := runtime.CreateSession(context.Background(), "site", []string{"192.168.13.0/24"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rejection := protocol.StopSessionPayload{SessionID: 81, Reason: string(protocol.ErrorSiteNoRoute)}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlStopSession, "stale-request", rejection)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.CreateSession(context.Background(), "site", []string{"192.168.21.0/24"}); err == nil {
|
|
t.Fatal("stale rejection released the active pending request")
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlStopSession, requestID, rejection)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.CreateSession(context.Background(), "site", []string{"192.168.21.0/24"}); err != nil {
|
|
t.Fatalf("matching PREPARE rejection did not release pending state: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSiteRuntimePrepareActivateAndCleanup(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.2")
|
|
control := &fakeNodeControl{}
|
|
gateway := &fakeGateway{}
|
|
runtime, err := NewSiteRuntime(context.Background(), SiteConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.2"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"),
|
|
UDPPort: port, StatsInterval: time.Hour, Routes: &fakeRoutes{lookup: route.LookupDirect},
|
|
Gateway: gateway, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
prepare := protocol.PrepareSessionPayload{SessionID: 77, EngineerOverlayIP: "127.0.0.1", TargetCIDRs: []string{"192.168.13.0/24"}}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlPrepareSession, "", prepare)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := control.last(t, protocol.ControlPrepareResult).payload.(protocol.PrepareResultPayload)
|
|
if !result.OK || result.SubnetGatewayStatus != "netstack" || gateway.prepared != 77 || len(result.RouteResults) != 1 {
|
|
t.Fatalf("PREPARE_RESULT=%+v gateway=%d", result, gateway.prepared)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlSessionActive, "", protocol.SessionActivePayload{SessionID: 77})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.Egress(context.Background(), 77, testRuntimeIPv4("192.168.13.10", "127.0.0.1")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlStopSession, "", protocol.StopSessionPayload{SessionID: 77})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gateway.closed != 77 {
|
|
t.Fatalf("closed gateway Session = %d", gateway.closed)
|
|
}
|
|
}
|
|
|
|
func TestSiteRuntimeRejectsDefaultOnlyRoute(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.2")
|
|
control := &fakeNodeControl{}
|
|
gateway := &fakeGateway{}
|
|
runtime, err := NewSiteRuntime(context.Background(), SiteConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.2"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"),
|
|
UDPPort: port, StatsInterval: time.Hour, Routes: &fakeRoutes{lookup: route.LookupDefaultOnly},
|
|
Gateway: gateway, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
prepare := protocol.PrepareSessionPayload{SessionID: 78, EngineerOverlayIP: "127.0.0.1", TargetCIDRs: []string{"192.168.13.0/24"}}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlPrepareSession, "", prepare)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := control.last(t, protocol.ControlPrepareResult).payload.(protocol.PrepareResultPayload)
|
|
if result.OK || result.ErrorCode != protocol.ErrorSiteNoRoute || gateway.prepared != 0 {
|
|
t.Fatalf("PREPARE_RESULT=%+v gateway=%d", result, gateway.prepared)
|
|
}
|
|
}
|
|
|
|
func TestSiteRuntimeRejectsPrepareAtFlowCapacity(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
tcpFlows int
|
|
udpFlows int
|
|
tcpLimit int
|
|
udpLimit int
|
|
}{
|
|
{name: "tcp", tcpFlows: 2, tcpLimit: 2, udpLimit: 4},
|
|
{name: "udp", udpFlows: 4, tcpLimit: 2, udpLimit: 4},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.2")
|
|
control := &fakeNodeControl{}
|
|
gateway := &fakeGateway{tcpFlows: test.tcpFlows, udpFlows: test.udpFlows}
|
|
runtime, err := NewSiteRuntime(context.Background(), SiteConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.2"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"),
|
|
UDPPort: port, TCPFlowLimit: test.tcpLimit, UDPFlowLimit: test.udpLimit, StatsInterval: time.Hour,
|
|
Routes: &fakeRoutes{lookup: route.LookupDirect}, Gateway: gateway, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
prepare := protocol.PrepareSessionPayload{SessionID: 79, EngineerOverlayIP: "127.0.0.1", TargetCIDRs: []string{"192.168.13.0/24"}}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlPrepareSession, "", prepare)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := control.last(t, protocol.ControlPrepareResult).payload.(protocol.PrepareResultPayload)
|
|
if result.OK || result.ErrorCode != protocol.ErrorFlowLimitReached || gateway.prepared != 0 {
|
|
t.Fatalf("PREPARE_RESULT=%+v gateway=%d", result, gateway.prepared)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEngineerRuntimeInjectionFailureStopsOnlySession(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.1")
|
|
control := &fakeNodeControl{}
|
|
device := &fakePacketDevice{injectErr: errors.New("wintun write failed")}
|
|
runtime, err := NewEngineerRuntime(context.Background(), EngineerConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.1"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"), UDPPort: port,
|
|
StatsInterval: time.Hour, Routes: &fakeRoutes{}, Device: device, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
if _, err := runtime.CreateSession(context.Background(), "site", []string{"192.168.13.0/24"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlSessionConfig, control.last(t, protocol.ControlCreateSession).requestID, protocol.SessionConfigPayload{
|
|
SessionID: 91, PeerOverlayIP: "127.0.0.2", CIDRs: []string{"192.168.13.0/24"}, MTU: 1280, UDPPort: port,
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.inject(context.Background(), 91, testRuntimeIPv4("192.168.13.10", "127.0.0.1")); err == nil {
|
|
t.Fatal("injection failure was hidden")
|
|
}
|
|
stop := control.last(t, protocol.ControlStopSession).payload.(protocol.StopSessionPayload)
|
|
if stop.Reason != string(protocol.ErrorSessionInjectFailed) || runtime.Snapshot().ID != 0 || device.router != nil {
|
|
t.Fatalf("STOP=%+v snapshot=%+v router=%v", stop, runtime.Snapshot(), device.router)
|
|
}
|
|
}
|
|
|
|
func TestSiteRuntimeInjectionFailureClosesGatewaySession(t *testing.T) {
|
|
port := availableUDPPort(t, "127.0.0.2")
|
|
control := &fakeNodeControl{}
|
|
gateway := &fakeGateway{injectErr: errors.New("netstack inject failed")}
|
|
runtime, err := NewSiteRuntime(context.Background(), SiteConfig{
|
|
LocalOverlayIP: netip.MustParseAddr("127.0.0.2"), OverlayCIDR: netip.MustParsePrefix("127.0.0.0/8"), UDPPort: port,
|
|
StatsInterval: time.Hour, Routes: &fakeRoutes{lookup: route.LookupDirect}, Gateway: gateway, Control: control,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer runtime.Close()
|
|
prepare := protocol.PrepareSessionPayload{SessionID: 92, EngineerOverlayIP: "127.0.0.1", TargetCIDRs: []string{"192.168.13.0/24"}}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlPrepareSession, "", prepare)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.HandleControl(context.Background(), envelope(t, protocol.ControlSessionActive, "", protocol.SessionActivePayload{SessionID: 92})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.inject(context.Background(), 92, testRuntimeIPv4("127.0.0.1", "192.168.13.10")); err == nil {
|
|
t.Fatal("injection failure was hidden")
|
|
}
|
|
stop := control.last(t, protocol.ControlStopSession).payload.(protocol.StopSessionPayload)
|
|
if stop.Reason != string(protocol.ErrorSessionInjectFailed) || gateway.closed != 92 {
|
|
t.Fatalf("STOP=%+v gateway.closed=%d", stop, gateway.closed)
|
|
}
|
|
}
|
|
|
|
type fakeNodeControl struct {
|
|
mu sync.Mutex
|
|
messages []nodeMessage
|
|
}
|
|
|
|
type nodeMessage struct {
|
|
messageType protocol.ControlMessageType
|
|
requestID string
|
|
payload any
|
|
}
|
|
|
|
func (f *fakeNodeControl) Send(_ context.Context, messageType protocol.ControlMessageType, requestID string, payload any) error {
|
|
f.mu.Lock()
|
|
f.messages = append(f.messages, nodeMessage{messageType: messageType, requestID: requestID, payload: payload})
|
|
f.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeNodeControl) last(t *testing.T, messageType protocol.ControlMessageType) nodeMessage {
|
|
t.Helper()
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
for index := len(f.messages) - 1; index >= 0; index-- {
|
|
if f.messages[index].messageType == messageType {
|
|
return f.messages[index]
|
|
}
|
|
}
|
|
t.Fatalf("no %s message", messageType)
|
|
return nodeMessage{}
|
|
}
|
|
|
|
func (f *fakeNodeControl) count(messageType protocol.ControlMessageType) int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
count := 0
|
|
for _, message := range f.messages {
|
|
if message.messageType == messageType {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
type fakePacketDevice struct {
|
|
router *clientwg.PacketMux
|
|
injectErr error
|
|
}
|
|
|
|
func (f *fakePacketDevice) SetPacketMux(router *clientwg.PacketMux) { f.router = router }
|
|
func (f *fakePacketDevice) InjectInbound([]byte) error { return f.injectErr }
|
|
|
|
type fakeRoutes struct {
|
|
added, removed []netip.Prefix
|
|
conflict bool
|
|
lookup route.LookupResult
|
|
}
|
|
|
|
func (f *fakeRoutes) AddRemote(prefix netip.Prefix) error {
|
|
f.added = append(f.added, prefix)
|
|
return nil
|
|
}
|
|
func (f *fakeRoutes) RemoveRemote(prefix netip.Prefix) error {
|
|
f.removed = append(f.removed, prefix)
|
|
return nil
|
|
}
|
|
func (f *fakeRoutes) Conflicts(prefix netip.Prefix) ([]route.Entry, error) {
|
|
if f.conflict {
|
|
return []route.Entry{{Destination: netip.MustParsePrefix("192.168.0.0/16")}}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
func (*fakeRoutes) Reconcile() error { return nil }
|
|
func (f *fakeRoutes) Lookup(netip.Addr) (route.LookupResult, error) {
|
|
if f.lookup == "" {
|
|
return route.LookupNoRoute, nil
|
|
}
|
|
return f.lookup, nil
|
|
}
|
|
|
|
type fakeGateway struct {
|
|
prepared, closed uint64
|
|
injectErr error
|
|
tcpFlows int
|
|
udpFlows int
|
|
}
|
|
|
|
func (f *fakeGateway) Prepare(_ context.Context, config subnetgateway.SessionConfig) error {
|
|
f.prepared = config.SessionID
|
|
return nil
|
|
}
|
|
func (f *fakeGateway) InjectIPv4(context.Context, uint64, []byte) error { return f.injectErr }
|
|
func (f *fakeGateway) CloseSession(_ context.Context, id uint64) error {
|
|
f.closed = id
|
|
return nil
|
|
}
|
|
func (f *fakeGateway) FlowCounts() (int, int) { return f.tcpFlows, f.udpFlows }
|
|
func (*fakeGateway) Close() error { return nil }
|
|
|
|
func availableUDPPort(t *testing.T, host string) int {
|
|
t.Helper()
|
|
connection, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP(host), Port: 0})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := connection.LocalAddr().(*net.UDPAddr).Port
|
|
if err := connection.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return port
|
|
}
|
|
|
|
func testRuntimeIPv4(sourceText, destinationText string) []byte {
|
|
packet := make([]byte, 20)
|
|
packet[0] = 0x45
|
|
packet[3] = 20
|
|
source := netip.MustParseAddr(sourceText).As4()
|
|
destination := netip.MustParseAddr(destinationText).As4()
|
|
copy(packet[12:16], source[:])
|
|
copy(packet[16:20], destination[:])
|
|
return packet
|
|
}
|