From 3d51aa455006903345f554a2dd90034993796114 Mon Sep 17 00:00:00 2001 From: sergei Date: Tue, 14 Apr 2026 06:23:55 +0400 Subject: vpnem: VPN infrastructure with load-balanced multi-protocol nodes - 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 --- test_balancing.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 test_balancing.py (limited to 'test_balancing.py') diff --git a/test_balancing.py b/test_balancing.py new file mode 100644 index 0000000..0f7933d --- /dev/null +++ b/test_balancing.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Real-world load balancing test for vpnem API.""" +import urllib.request +import json +import sys + +BASE = "https://vpn.em-sysadmin.xyz" + +def req(path, method="GET", data=None, headers=None): + """Make HTTP request.""" + url = BASE + path + h = headers or {} + h["Content-Type"] = "application/json" + body = json.dumps(data).encode() if data else None + rq = urllib.request.Request(url, data=body, headers=h, method=method) + try: + resp = urllib.request.urlopen(rq, timeout=10) + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as e: + print(f" ERROR {e.code}: {e.read().decode()[:200]}") + return None + except Exception as e: + print(f" ERROR: {e}") + return None + +def get_recommend(client_ip): + """Get recommendation for a client IP.""" + return req("/api/v1/recommend", headers={"X-Forwarded-For": client_ip}) + +def connect(client_ip, server_ip, node_id): + """Report connection.""" + return req("/api/v1/connect", method="POST", data={ + "server_ip": server_ip, "node_id": node_id + }, headers={"X-Forwarded-For": client_ip}) + +def disconnect(client_ip): + """Report disconnection.""" + return req("/api/v1/disconnect", method="POST", data={ + "server_ip": "" + }, headers={"X-Forwarded-For": client_ip}) + +def main(): + print("=" * 70) + print("VPNEM Load Balancing — Real Server Test") + print("=" * 70) + print() + + # Step 1: Test recommendations for multiple "studios" + print("[1] Testing recommendations for 5 different studios...") + print() + studios = [ + ("195.10.20.1", "Barnaul Studio 1"), + ("195.10.20.2", "Barnaul Studio 2"), + ("91.50.60.1", "Moscow Studio"), + ("46.30.20.1", "Novosibirsk Studio"), + ("178.120.1.1", "Test Studio"), + ] + + recommendations = {} + for ip, name in studios: + rec = get_recommend(ip) + if rec: + server = rec.get("recommended_server_ip", "none") + reason = rec.get("reason", "unknown") + load = rec.get("load_info", "no info") + recommendations[ip] = server + print(f" {name:25s} → {server:15s} ({reason})") + print(f" Load: {load}") + else: + print(f" {name:25s} → ERROR") + + print() + + # Step 2: Check distribution — do different studios get different servers? + print("[2] Distribution check...") + servers_used = list(recommendations.values()) + unique = set(servers_used) + print(f" Studios: {len(studios)}, Recommended IPs: {len(unique)}") + print(f" Servers used: {', '.join(unique)}") + if len(unique) > 1: + print(" ✅ GOOD — different studios get different servers") + else: + print(" ⚠️ All studios got the same server (possible if load is equal)") + + print() + + # Step 3: Simulate connections + print("[3] Simulating connections...") + nodes = { + "5.180.97.181": "nl-multi-181", + "5.180.97.197": "nl-multi-197", + "5.180.97.198": "nl-multi-198", + "5.180.97.199": "nl-multi-199", + } + for ip, name in studios: + srv = recommendations.get(ip) + nid = nodes.get(srv, "nl-multi-181") + resp = connect(ip, srv, nid) + if resp: + rec_srv = resp.get("recommended_server_ip", "none") + print(f" {name:25s} connected → {srv:15s} (next rec: {rec_srv})") + else: + print(f" {name:25s} connect FAILED") + + print() + + # Step 4: Check recommendations AFTER connections (should shift) + print("[4] Recommendations AFTER connections (should re-balance)...") + new_ip = "100.100.100.1" + rec = get_recommend(new_ip) + if rec: + srv = rec.get("recommended_server_ip", "none") + reason = rec.get("reason", "unknown") + load = rec.get("load_info", "no info") + print(f" New Studio ({new_ip}) → {srv:15s} ({reason})") + print(f" Load: {load}") + # The new studio should NOT get the most loaded server + else: + print(f" ERROR getting recommendation") + + print() + + # Step 5: Disconnect one studio, check if recommendation changes + print("[5] Disconnecting first studio and checking rebalance...") + disc_ip = studios[0][0] + disconnect(disc_ip) + print(f" Disconnected {studios[0][1]} ({disc_ip})") + + rec = get_recommend("200.200.200.1") + if rec: + srv = rec.get("recommended_server_ip", "none") + load = rec.get("load_info", "no info") + print(f" New Studio (200.200.200.1) → {srv:15s}") + print(f" Load: {load}") + else: + print(" ERROR") + + print() + + # Step 6: Clean up + print("[6] Cleaning up — disconnecting all test studios...") + for ip, name in studios: + disconnect(ip) + disconnect(new_ip) + disconnect("200.200.200.1") + print(" Done") + + print() + print("=" * 70) + print("Test complete.") + print("=" * 70) + +if __name__ == "__main__": + main() -- cgit v1.2.3