summaryrefslogtreecommitdiff
path: root/internal/rules/connections_test.go
diff options
context:
space:
mode:
authorsergei <sergei@em-sysadmin.xyz>2026-04-14 06:23:55 +0400
committersergei <sergei@em-sysadmin.xyz>2026-04-14 06:23:55 +0400
commit3d51aa455006903345f554a2dd90034993796114 (patch)
tree62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/rules/connections_test.go
downloadvpnem-3d51aa455006903345f554a2dd90034993796114.tar.gz
vpnem-3d51aa455006903345f554a2dd90034993796114.tar.bz2
vpnem-3d51aa455006903345f554a2dd90034993796114.zip
vpnem: VPN infrastructure with load-balanced multi-protocol nodesHEADmain
- Multi-protocol VPS nodes (VLESS-REALITY + Hysteria2 + SOCKS5) - Smart load balancing via recommendation API - Windows/Linux client (Go + Wails + sing-box) - Server API with RealIP detection and connection tracking - Auto-deployment via vpnui control plane - Silent Windows installer with UAC elevation - Load-based server recommendation (no sticky sessions) - Best Server one-click connection workflow
Diffstat (limited to 'internal/rules/connections_test.go')
-rw-r--r--internal/rules/connections_test.go201
1 files changed, 201 insertions, 0 deletions
diff --git a/internal/rules/connections_test.go b/internal/rules/connections_test.go
new file mode 100644
index 0000000..22355dc
--- /dev/null
+++ b/internal/rules/connections_test.go
@@ -0,0 +1,201 @@
+package rules
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestConnectionStoreConnectAndRecommend(t *testing.T) {
+ tmpDir := t.TempDir()
+ store := NewConnectionStore(tmpDir)
+ if err := store.Load(); err != nil {
+ t.Fatal(err)
+ }
+
+ availableIPs := []string{"5.180.97.198", "5.180.97.199", "5.180.97.197"}
+ healthyIPs := map[string]bool{"5.180.97.198": true, "5.180.97.199": true, "5.180.97.197": true}
+
+ // Studio 1 connects to 198
+ store.Connect("1.2.3.4", "5.180.97.198", "nl-198", "windows", "2.0.11")
+
+ // Studio 1 asks — should get 198 (least loaded: 1 client vs 0/0)
+ // Actually: 198=1, 199=0, 197=0 → should get 199 or 197 (least loaded)
+ rec1 := store.GetRecommendation("1.2.3.4", availableIPs, healthyIPs)
+ if rec1.RecommendedServerIP == "5.180.97.198" {
+ // This is OK if load balancing picks a different server
+ t.Logf("studio 1 recommended: %s (reason: %s)", rec1.RecommendedServerIP, rec1.Reason)
+ } else {
+ t.Logf("studio 1 recommended different server: %s (load-based)", rec1.RecommendedServerIP)
+ }
+
+ // Studio 2 is new — should also get least loaded
+ rec2 := store.GetRecommendation("9.9.9.9", availableIPs, healthyIPs)
+ if rec2.RecommendedServerIP == "" {
+ t.Fatal("expected recommendation for new studio")
+ }
+ t.Logf("studio 2 recommended: %s (reason: %s)", rec2.RecommendedServerIP, rec2.Reason)
+}
+
+func TestPureLoadBalancing(t *testing.T) {
+ tmpDir := t.TempDir()
+ store := NewConnectionStore(tmpDir)
+ if err := store.Load(); err != nil {
+ t.Fatal(err)
+ }
+
+ availableIPs := []string{"5.180.97.198", "5.180.97.199", "5.180.97.197"}
+ healthyIPs := map[string]bool{"5.180.97.198": true, "5.180.97.199": true, "5.180.97.197": true}
+
+ // 3 studios connect to 198 (overload it)
+ for i := 0; i < 3; i++ {
+ ip := "10.0.0." + string(rune('1'+i))
+ store.Connect(ip, "5.180.97.198", "nl-198", "windows", "")
+ }
+
+ // New studio should NOT get 198 (3 clients) — should get 199 or 197 (0 clients)
+ rec := store.GetRecommendation("99.99.99.99", availableIPs, healthyIPs)
+ if rec.RecommendedServerIP == "5.180.97.198" {
+ t.Fatalf("should not recommend overloaded server, got %s", rec.RecommendedServerIP)
+ }
+ t.Logf("new studio recommended: %s (reason: %s)", rec.RecommendedServerIP, rec.Reason)
+
+ // Even studio 1 (home=198) should get load-balanced recommendation
+ rec2 := store.GetRecommendation("10.0.0.1", availableIPs, healthyIPs)
+ t.Logf("studio 1 re-recommended: %s (reason: %s, isRebalance: %v)",
+ rec2.RecommendedServerIP, rec2.Reason, rec2.IsRebalance)
+}
+
+func TestHomeServerUnhealthy(t *testing.T) {
+ tmpDir := t.TempDir()
+ store := NewConnectionStore(tmpDir)
+ if err := store.Load(); err != nil {
+ t.Fatal(err)
+ }
+
+ availableIPs := []string{"5.180.97.198", "5.180.97.199"}
+ // 198 is NOT healthy
+ healthyIPs := map[string]bool{"5.180.97.199": true}
+
+ // Studio 1 has connected to 198
+ store.Connect("1.2.3.4", "5.180.97.198", "nl-198", "windows", "")
+
+ // But 198 is unhealthy — should recommend 199
+ rec := store.GetRecommendation("1.2.3.4", availableIPs, healthyIPs)
+ if rec.RecommendedServerIP == "5.180.97.198" {
+ t.Fatalf("should not recommend unhealthy server, got %s", rec.RecommendedServerIP)
+ }
+ if rec.RecommendedServerIP != "5.180.97.199" {
+ t.Fatalf("should recommend healthy server 199, got %s", rec.RecommendedServerIP)
+ }
+}
+
+func TestDisconnect(t *testing.T) {
+ tmpDir := t.TempDir()
+ store := NewConnectionStore(tmpDir)
+ if err := store.Load(); err != nil {
+ t.Fatal(err)
+ }
+
+ availableIPs := []string{"5.180.97.198"}
+
+ store.Connect("1.2.3.4", "5.180.97.198", "nl-198", "windows", "")
+
+ load := store.GetLoadInfo(availableIPs)
+ if len(load) == 0 || load[0].ActiveClients != 1 {
+ t.Fatalf("expected 1 active client, got %v", load)
+ }
+
+ store.Disconnect("1.2.3.4")
+
+ load = store.GetLoadInfo(availableIPs)
+ if len(load) == 0 || load[0].ActiveClients != 0 {
+ t.Fatalf("expected 0 active clients after disconnect, got %v", load)
+ }
+}
+
+func TestSessionExpiry(t *testing.T) {
+ tmpDir := t.TempDir()
+ store := NewConnectionStore(tmpDir)
+ store.staleAfter = 1 * time.Millisecond
+ if err := store.Load(); err != nil {
+ t.Fatal(err)
+ }
+
+ availableIPs := []string{"5.180.97.198"}
+ healthyIPs := map[string]bool{"5.180.97.198": true}
+
+ store.Connect("1.2.3.4", "5.180.97.198", "nl-198", "windows", "")
+ time.Sleep(10 * time.Millisecond)
+
+ rec := store.GetRecommendation("1.2.3.4", availableIPs, healthyIPs)
+ if rec.RecommendedServerIP != "5.180.97.198" {
+ t.Fatalf("expected recommendation to 198 after session expiry, got %s", rec.RecommendedServerIP)
+ }
+
+ load := store.GetLoadInfo(availableIPs)
+ if len(load) == 0 || load[0].ActiveClients != 0 {
+ t.Fatalf("expected 0 active clients after expiry, got %v", load)
+ }
+}
+
+func TestPersistence(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ store1 := NewConnectionStore(tmpDir)
+ store1.Connect("1.2.3.4", "5.180.97.199", "nl-199", "windows", "")
+
+ store2 := NewConnectionStore(tmpDir)
+ if err := store2.Load(); err != nil {
+ t.Fatal(err)
+ }
+
+ availableIPs := []string{"5.180.97.199"}
+ healthyIPs := map[string]bool{"5.180.97.199": true}
+ rec := store2.GetRecommendation("1.2.3.4", availableIPs, healthyIPs)
+ if rec.RecommendedServerIP != "5.180.97.199" {
+ t.Fatalf("expected recommendation to 199, got %s", rec.RecommendedServerIP)
+ }
+
+ _, err := os.Stat(filepath.Join(tmpDir, "connections.json"))
+ if err != nil {
+ t.Fatal("expected connections.json to exist")
+ }
+}
+
+func TestLoadInfoFormat(t *testing.T) {
+ tmpDir := t.TempDir()
+ store := NewConnectionStore(tmpDir)
+ store.maxCap = 10
+
+ store.Connect("1.1.1.1", "5.180.97.198", "nl-198", "windows", "")
+ store.Connect("2.2.2.2", "5.180.97.198", "nl-198", "windows", "")
+ store.Connect("3.3.3.3", "5.180.97.199", "nl-199", "linux", "")
+
+ availableIPs := []string{"5.180.97.198", "5.180.97.199"}
+ load := store.GetLoadInfo(availableIPs)
+
+ if len(load) != 2 {
+ t.Fatalf("expected 2 server load entries, got %d", len(load))
+ }
+
+ for _, info := range load {
+ if info.ServerIP == "5.180.97.198" {
+ if info.ActiveClients != 2 {
+ t.Errorf("expected 2 clients on 198, got %d", info.ActiveClients)
+ }
+ if info.LoadPercent != 20 {
+ t.Errorf("expected 20%% load on 198, got %d", info.LoadPercent)
+ }
+ }
+ if info.ServerIP == "5.180.97.199" {
+ if info.ActiveClients != 1 {
+ t.Errorf("expected 1 client on 199, got %d", info.ActiveClients)
+ }
+ if info.LoadPercent != 10 {
+ t.Errorf("expected 10%% load on 199, got %d", info.LoadPercent)
+ }
+ }
+ }
+}