From 2d0628c5bb9ddeaa363ea8e0f5b13873c51585f0 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sun, 2 Aug 2026 15:38:42 +0300 Subject: [PATCH] docs: update SERVER_DEPLOYMENT.md for V2 domain-free architecture --- SERVER_DEPLOYMENT.md | 474 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 SERVER_DEPLOYMENT.md diff --git a/SERVER_DEPLOYMENT.md b/SERVER_DEPLOYMENT.md new file mode 100644 index 0000000..62b4ba1 --- /dev/null +++ b/SERVER_DEPLOYMENT.md @@ -0,0 +1,474 @@ +# ReVoltVPN — Server Deployment Guide v7.0 (XHTTP + Reality + Domain-Free) + +> **Last verified:** 2026-08-02 against live Xray docs at `https://xtls.github.io/en/` +> **Transport:** XHTTP (H2 stream-up) + Reality — NOT TCP, NOT XTLS-Vision +> **Spoof target:** `www.github.com` (single site, Cloudflare-backed) +> **Client:** Domain-free — API routed through tunnel, no domain in APK. See `DOCUMENTATION-ARCHITECTURE-V2.md`. + +--- + +## Connection Flow — End to End + +### Step 0: Bootstrap (V2 — domain-free) + +Before the normal session flow, the app connects with a hardcoded Reality config +bundled in the APK (`bootstrapVlessUrl`). This bootstrap client is permanently +defined in `xray_config_reality.json` with `email: "bootstrap"`. Xray routing +restricts it to `10.254.254.0/24` — only the internal API, zero internet access. + +``` +App → bootstrap tunnel (Reality, github.com SNI) → polls 10.254.254.1:5000 + ↓ + Xray "api" outbound + (redirect: 127.0.0.1:5000) + ↓ + Flask Hivemind +``` + +The app fetches the real per-session VLESS URL through the bootstrap tunnel, +disconnects, then reconnects with the real config. From now on, ALL API calls +(health, sync, stop) go through `10.254.254.1:5000` inside the tunnel. +**Phone never makes a connection to any domain.** + +### Step 1: Client taps Connect + +``` +App → AdMob SSV → server creates VLESS UUID → app bootstraps → polls inside tunnel + +### Step 2: Server returns session config + +The `/session/status` response (from `hivemind/api.py`): + +```json +{ + "active": true, + "vless_uuid": "550e8400-e29b-41d4-a716-446655440000", + "vless_ip": "204.168.246.88", + "vless_port": 8443, + "reality_pbk": "fHyNQm1UbaFD3wRQn4AAx5SdVzDCzlxm8G4xprpWymI=", + "reality_sid": "abc123", + "reality_sni": "www.github.com", + "reality_fp": "chrome", + "xhttp_path": "/revolt", + "expires_in_seconds": 3540, + "nonce": "1234567890-1716480000", + "is_throttled": false +} +``` + +Every Reality parameter comes from the server — client has no hardcoded SNI or path. + +### Step 3: Client builds VLESS URL + +From these response fields, `hivemind_service.dart` builds: + +``` +vless://550e8400-e29b-41d4-a716-446655440000@204.168.246.88:8443 + ?security=reality + &type=xhttp + &path=%2Frevolt + &pbk=fHyNQm1UbaFD3wRQn4AAx5SdVzDCzlxm8G4xprpWymI%3D + &sni=www.github.com + &sid=abc123 + &fp=chrome + #ReVoltVPN +``` + +**Every parameter verified against live docs:** + +| Parameter | Live doc source | Required? | Value | +|-----------|----------------|-----------|-------| +| `security=reality` | Reality docs: *"REALITY can only be used together with RAW, XHTTP, and gRPC"* | Yes | `reality` | +| `type=xhttp` | XHTTP docs: transport name | Yes | `xhttp` | +| `path=/revolt` | XHTTP docs: *"只需填 path,其它不填即可"* | Yes | server-defined | +| `pbk=...` | Reality docs: *"password: string — Required. The public key corresponding to the server private key."* | Yes | from `xray x25519` | +| `sni=...` | Reality docs: *"serverName: string — One of the server-side serverNames."* | Yes | from `REALITY_SNI_POOL` | +| `sid=...` | Reality docs: *"shortId: string — One of the server-side shortIds."* | Yes | `abc123` | +| `fp=chrome` | Reality docs: *"fingerprint: string — One of chrome, firefox, safari, random..."* | Yes | `chrome` | +| `host=` | XHTTP docs: *"建议没事别设"* (don't set unless needed) | No | intentionally omitted | +| `flow=` | VLESS docs: *"XTLS only available: TCP+TLS/REALITY"* (NOT XHTTP) | No | intentionally omitted | + +### Step 4: flutter_vless generates Xray client config + +`flutter_vless` 1.1.4 parses the VLESS URL and produces the equivalent of: + +```json +{ + "outbounds": [{ + "protocol": "vless", + "settings": { + "vnext": [{ + "address": "204.168.246.88", + "port": 8443, + "users": [{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "encryption": "none", + "flow": "" + }] + }] + }, + "streamSettings": { + "network": "xhttp", + "security": "reality", + "xhttpSettings": { + "path": "/revolt" + }, + "realitySettings": { + "serverName": "www.github.com", + "fingerprint": "chrome", + "publicKey": "fHyNQm1UbaFD3wRQn4AAx5SdVzDCzlxm8G4xprpWymI=", + "shortId": "abc123", + "spiderX": "" + } + } + }] +} +``` + +### Step 5: TLS handshake (Reality) + +``` +Client Server (Xray) + | | + |── ClientHello ────────────────────→| + | SNI: www.github.com | + | ALPN: h2 (H2 default w/ Reality) | + | random session ID | + | | + |←── ServerHello ────────────────────| + | Certificate: github.com's REAL | + | cert (fetched from | + | www.github.com:443 by Xray) | + | Signed with Reality privateKey | + | | + |── auth: pbk + shortId ───────────→| + | (embedded in TLS extension) | + | | + |←── TLS 1.3 handshake complete ────| +``` + +To any DPI box, this looks exactly like a browser connecting to GitHub. + +### Step 6: HTTP/2 + VLESS inside the TLS tunnel + +``` +Inside the TLS tunnel: + ┌─────────────────────────────────┐ + │ HTTP/2 connection │ + │ :method: POST │ + │ :path: /revolt │ ← XHTTP path matching + │ :authority: www.github.com │ ← Host header (SNI fallback) + │ │ + │ ┌───────────────────────────┐ │ + │ │ VLESS payload │ │ + │ │ UUID auth: ✓ │ │ + │ │ Actual VPN traffic │ │ + │ └───────────────────────────┘ │ + └─────────────────────────────────┘ +``` + +### Step 7: Xray routes traffic + +``` +Phone → Xray Reality (8443) + │ + ├─ dest: 10.254.254.0/24 → api outbound (redirect → Flask 127.0.0.1:5000) + ├─ user: bootstrap, other dest → block (blackhole) + ├─ port: 25,465,587 → block (SMTP) + ├─ port: 135-139,445 → block (NetBIOS) + ├─ port: 1900 → block (SSDP) + └─ default → direct (freedom → internet) +``` + +--- + +## Will it work? — Answer + +**Yes, if these 5 things are true on the server:** + +1. ✅ `xray_config_reality.json` deployed to `/usr/local/etc/xray/config.json` with the real private key +2. ✅ `hivemind/config.py` has the matching public key in `REALITY_PUBLIC_KEY` +3. ✅ Bootstrap UUID generated (`xray uuid`) and placed in BOTH `xray_config_reality.json` AND `app_config.dart` +4. ✅ Xray restarted after config deploy +5. ✅ Firewall allows inbound 8443 (and 8444 for throttled) + +**The config is internally consistent.** Every parameter flows from a single source: +- `config.py` → API → client → `flutter_vless` → Xray client config +- `config.json` → Xray server inbound + +If those match (and they do in this repo), the tunnel will establish. + +--- + +## Server Setup (clean deploy) + +### 1. Install Xray + +```bash +bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install +``` + +### 2. Generate Reality keypair + +```bash +xray x25519 +# Output: +# Private key: <44-char base64> ← goes in xray_config_reality.json +# Public key: <44-char base64> ← goes in hivemind/config.py +``` + +### 3. Generate bootstrap UUID + +```bash +xray uuid +# Output: 6ba7b810-9dad-11d1-80b4-00c04fd430c8 +``` + +Put this UUID in TWO places: +1. `xray_config_reality.json` — replace `BOOTSTRAP-UUID-REPLACE-ME` in the bootstrap client's `"id"` field +2. `lib/logic/app_config.dart` — replace `BOOTSTRAP-UUID-GOES-HERE` in `bootstrapUuid` + +### 4. Deploy Xray config + +Copy `xray_config_reality.json` from this repo → `/usr/local/etc/xray/config.json` on the server. + +Replace the private key with the one you just generated: +```json +"privateKey": "YOUR_GENERATED_PRIVATE_KEY" +``` + +```bash +chmod 600 /usr/local/etc/xray/config.json +systemctl restart xray +systemctl status xray # verify: active (running) +``` + +### 5. Verify Xray is listening + +```bash +ss -tlnp | grep -E '8443|8444' +# Should show: +# LISTEN 0.0.0.0:8443 (Xray — full speed) +# LISTEN 0.0.0.0:8444 (Xray — throttled) +``` + +### 6. Verify the spoof cert is reachable + +```bash +xray tls ping www.github.com:443 +# Should show: certificate size > 3500 bytes (good for Reality) +``` + +### 7. Deploy Python Hivemind + +Upload to `/root/`: +``` +server_hivemind_5_0.py ← entry point +hivemind/ ← package (8 files) + ├── config.py ← EDIT: public key, server IP + ├── sessions.py + ├── xray.py + ├── admob.py + ├── management.py + ├── api.py + ├── main.py + └── __init__.py +``` + +### 8. Edit hivemind/config.py + +```python +REALITY_PUBLIC_KEY = "YOUR_GENERATED_PUBLIC_KEY" # from xray x25519 +VLESS_SERVER_IP = "YOUR_SERVER_IP" # Hetzner/VPS IP +REALITY_SNI_POOL = ["www.github.com"] # spoof target +VLESS_XHTTP_PATH = "/revolt" # must match config.json +``` + +### 9. Install Python dependencies + +```bash +pip install flask requests +``` + +### 10. Create systemd unit + +`/etc/systemd/system/hivemind.service`: +```ini +[Unit] +Description=ReVoltVPN Hivemind +After=network.target xray.service + +[Service] +Type=simple +User=root +WorkingDirectory=/root +ExecStart=/usr/bin/python3 -m gunicorn -w 1 -b 127.0.0.1:5000 hivemind.main:app +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +systemctl daemon-reload +systemctl enable --now hivemind +systemctl status hivemind +``` + +### 11. Firewall + +```bash +# Only open what's needed +ufw allow 443/tcp # nginx HTTPS (API) +ufw allow 8443/tcp # Xray Reality (full speed) +ufw allow 8444/tcp # Xray Reality (throttled) +ufw allow 80/tcp # certbot HTTP challenge +ufw enable + +# Keep these internal-only +# 5000 — Flask (localhost only, via nginx) +# 10085 — Xray API (localhost only) +``` + +### 12. nginx (AdMob SSV + website) + +nginx now serves only two purposes — the app itself never connects to it: +- AdMob SSV callbacks (`/api/admob/callback`) — Google → your server, not phone → server +- Branding website at the root — user's browser, manually + +Xray handles all app-to-server traffic through the Reality tunnel. + +`/etc/nginx/sites-available/revolt`: +```nginx +server { + listen 443 ssl http2; + server_name YOUR_DOMAIN; + + ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem; + + location /api/ { + proxy_pass http://127.0.0.1:5000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + root /var/www/revolt; + index index.html; + } +} + +server { + listen 80; + server_name YOUR_DOMAIN; + return 301 https://$host$request_uri; +} +``` + +### 13. certbot (Let's Encrypt) + +```bash +apt install certbot python3-certbot-nginx +certbot --nginx -d YOUR_DOMAIN +``` + +--- + +## Smoke Test + +### From the server: + +```bash +# 1. Is Xray running? +systemctl status xray + +# 2. Is Hivemind running? +systemctl status hivemind + +# 3. Can Hivemind reach Xray API? +curl http://127.0.0.1:10085/stats/query -d '{"pattern":"","reset":false}' + +# 4. Is Flask alive internally? +curl http://127.0.0.1:5000/api/health +# → {"ok": true} + +# 5. Does the API route through Xray work? (simulate tunnel → 10.254.254.1 → Flask) +curl http://10.254.254.1:5000/api/health +# → should fail (only reachable through Xray tunnel, not directly) + +# 6. Does the spoof target work? +xray tls ping www.github.com:443 +``` + +### From a client (after building the Flutter app): + +The app no longer hits any domain. All API calls go through the tunnel. +Verify by building and running the app in debug mode (`ADMOB_BYPASS=True`). + +```bash +# 7. Tap Connect → app bootstraps → polls inside tunnel → reconnects → timer starts +# 8. Health dot turns green (API reachable through tunnel) +# 9. Session timer syncs (5 second countdown updates) +# 10. Tap Disconnect → timer stops → cleanup POST goes through tunnel → tunnel drops +``` + +### Full integration test: + +``` +11. Trigger 4 GB → timer shows port-swap → reconnects through bootstrap → speed reduced +12. Support ad top-up → +30 min +2 GB → timer updates without reconnect +13. Kill app while connected → reopen → timer resumes (startup restoration) +``` + +--- + +## Troubleshooting + +| Symptom | Check | +|---------|-------| +| App says "Connecting..." forever | Server firewall blocking 8443? `ufw status` | +| App connects but no internet | Xray routing broken? Check `journalctl -u xray -f` | +| `/session/status` returns 500 | Hivemind crashed? `systemctl status hivemind` | +| `xray tls ping` fails | Server can't reach GitHub? DNS? IPv6? | +| Stats show 0 bytes | Users missing `email` field? Check `xray.py:add_client()` | +| Tunnel starts but immediately dies | `providerBundleIdentifier` mismatch? Must be `com.paladinvpn.app` | +| "Session not activated" in app | ADMOB_BYPASS not True? Or SSV callback not firing | +| App stuck on "Establishing secure channel…" | Bootstrap UUID mismatch between APK and xray_config? | +| Health dot gray after tunnel connects | `api` outbound not working? Check Xray routing rules | +| Bootstrap tunnel starts but no config | Hivemind not running on 127.0.0.1:5000? `systemctl status hivemind` | + +--- + +## Changing the spoof target + +If you ever want to switch from GitHub to another site: + +1. Edit `REALITY_SNI_POOL` in `hivemind/config.py` +2. Edit `serverNames` in **both** inbounds in `config.json` (add site + root domain) +3. Update `dest` in `config.json` to the new target +4. Verify: `xray tls ping NEW_TARGET:443` — cert must be > 3500 bytes +5. `systemctl restart xray` + +--- + +## Differences from old TCP/Vision setup + +| What | Old (SERVER_SETUP.md) | New (this doc) | +|------|----------------------|----------------| +| Transport | `tcp` | `xhttp` | +| Flow | `xtls-rprx-vision` | (none — XTLS doesn't work with XHTTP) | +| Spoof target | `www.microsoft.com` | `www.github.com` | +| Path | (none — TCP) | `/revolt` (XHTTP path) | +| Rate limiting | `uplinkOnly: 187500` (wrong) | `uplinkOnly: 2` (correct seconds), real cap via `tc` | +| shortIds | 3 entries | 1 entry matching `REALITY_SHORT_ID` | +| Fallback limits | Not present | Intentionally omitted (fingerprint risk per live docs) | +| `xhttp_path` in API | Not present | Server sends path, client reads it | +| `providerBundleIdentifier` | `com.revoltvpn.app` | `com.paladinvpn.app` | +| API transport | HTTPS to domain | Tunnel-internal to `10.254.254.1:5000` | +| Domain in APK | `getrevolt.app` | **None** — zero domain strings | +| Bootstrap | None (external API polling) | Hardcoded Reality config, tunnel-only API fetch | +| Xray outbounds | `direct` + `block` | `direct` + `block` + `api` (redirect → Flask) |