6.9 KiB
Приложение В. API endpoints — montana.quest
Public endpoints (доступны без аутентификации)
GET /vpn/sub
Назначение: возвращает универсальный VLESS link, base64-encoded.
Request:
GET /vpn/sub HTTP/1.1
Host: montana.quest
Response:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Cache-Control: no-store
dmxlc3M6Ly9lNmQzNTVlMi0yZDc5LTRjOTYtYTM3My0zYjBlNmI2ZjRiMGRAY2RuLm1vbnRhbmEucXVlc3Q6NDQzP2Zsb3c9eHRscy1ycHJ4LXZpc2lvbiZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmc2VjdXJpdHk9cmVhbGl0eSZmcD1jaHJvbWUmc25pPXd3dy5nb29nbGV0YWdtYW5hZ2VyLmNvbSZwYms9RWtUczJhR0tuRk5nRlowZjd3Z2Z0MnNKcDNWandGUXFJcndrWktNNGdEOCZzaWQ9MzAyODA1YmMwYzI1ZTUwNCMlQzklODglMjAlRDAlOUMlRDAlQkUlRDAlQkQlRDElODIlRDAlQjAlRDAlQkQlRDAlQjA=
decoded:
vless://e6d355e2-2d79-4c96-a373-3b0e6b6f4b0d@cdn.montana.quest:443?flow=xtls-rprx-vision&type=tcp&headerType=none&security=reality&fp=chrome&sni=www.googletagmanager.com&pbk=EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8&sid=302805bc0c25e504#Ɉ Монтана
POST /api/vpn/heartbeat
Назначение: регистрация активности клиента + начисление 0.001 Ɉ/сек.
Request:
POST /api/vpn/heartbeat HTTP/1.1
Host: montana.quest
Content-Type: application/json
Content-Length: <len>
{"address": "<40 hex chars>"}
Response (via Montana exit):
{
"ok": true,
"throttled": false,
"via_montana": true,
"node": "newyork",
"node_ip": "86.104.72.12",
"node_city": "Нью-Йорк",
"credited_seconds": 4.95,
"balance": 0.043,
"seconds": 43.5
}
Response (не через Montana exit):
{
"ok": false,
"reason": "not_via_montana_vpn",
"via_montana": false,
"node": null,
"node_ip": null,
"node_city": null
}
Throttling: если последний heartbeat < MIN_HEARTBEAT_INTERVAL (4.0 сек) назад — throttled: true, начисление 0.
Rate limit: нет (полагаемся на проверку via_montana IP-whitelist).
Errors:
400 invalid json— body не JSON400 bad address— address не соответствует[0-9a-f]{40}
GET /api/vpn/balance?address=<addr>
Назначение: read-only баланс.
Response:
{
"address": "2f8714b236118011647ec51d0ca6ad40d286bec7",
"balance": 0.043,
"seconds": 43.5,
"online": true,
"rate_per_second": 0.001,
"last_node": "newyork"
}
online = (now - last_hb) < 16 (4 × MIN_HEARTBEAT_INTERVAL).
GET /api/vpn/check
Назначение: диагностика — какой IP видит backend?
Response:
{
"your_ip": "86.104.72.12",
"via_montana": true,
"node": "newyork",
"node_city": "Нью-Йорк"
}
Полезно для клиента чтобы проверить что трафик действительно идёт через VPN.
GET /api/vpn/stats
Назначение: агрегированная статистика всей сети.
Response:
{
"wallets": 6,
"total_juno": 30.2,
"total_seconds": 30245.5,
"active_now": 2,
"rate_per_second": 0.001
}
active_now = count where (now - last_hb) < 60.
Admin endpoints (localhost-only)
POST /api/vpn/admin/purge
Назначение: удалить inactive 30+ days с нулевым балансом.
Restriction: if request.remote_addr not in ("127.0.0.1", "::1"): return 403.
Response:
{
"removed_count": 14,
"removed": ["anon9d98ace8...", "anonc95f62...", ...]
}
CORS configuration
@app.after_request add_cors(resp):
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Requested-With
Wide-open CORS — приемлемо для public read endpoints, рискованно для /heartbeat если бы там был cookie-based auth. На текущий момент heartbeat stateless, риск minimal.
nginx routing
/etc/nginx/sites-enabled/montana_quest:
server {
listen 443 ssl http2;
server_name montana.quest;
location = /vpn/sub {
proxy_pass http://127.0.0.1:5008;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header User-Agent $http_user_agent;
default_type "text/plain; charset=utf-8";
add_header Cache-Control "no-store" always;
}
location /api/vpn/ {
proxy_pass http://127.0.0.1:5008;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 8k;
}
location /vpn {
alias /var/www/montana_quest/vpn/;
try_files $uri $uri/ /vpn/index.html;
}
# ...
}
Heartbeat trace (полный)
Android device (Pixel)
↓ HeartbeatThread (Kotlin)
↓ socksConnect(127.0.0.1:10808, "montana.quest", 443)
LocalHost SOCKS5 (xray inbound на устройстве)
↓ routing: catchall → outbound "proxy"
↓ vless+reality к cdn.montana.quest:443 (= 91.132.142.42 Helsinki)
TCP к Helsinki:443
↓ TLS-1.3 Reality handshake (SNI = www.googletagmanager.com)
haproxy frontend reality_in
↓ stick-table lookup для source IP → backend = "us" (или другой)
↓ forward TCP к 127.0.0.1:40445
xray-pinned-us (Helsinki local)
↓ Reality decrypt + vless decrypt
↓ routing: inboundTag reality-in → outboundTag cascade-us
↓ vless+reality к 86.104.72.12:443 (NewYork)
TCP к NewYork:443
↓ Reality handshake
xray на NewYork
↓ routing: → outbound freedom
↓ TCP к montana.quest:443 (= 176.124.208.93 Moscow)
↓ Source IP видимый backend = 86.104.72.12 (NewYork IP)
nginx Moscow:443
↓ TLS-1.3 termination (Let's Encrypt cert)
↓ location /api/vpn/ → proxy_pass 127.0.0.1:5008
gunicorn (montana-vpn-balance.service)
↓ Flask routing → POST /api/vpn/heartbeat → heartbeat()
↓ client_ip() = 86.104.72.12 (через X-Forwarded-For не используется т.к. nginx ставит X-Real-IP)
↓ MONTANA_NODES.get("86.104.72.12") = ("newyork", "Нью-Йорк")
↓ with_state_lock(mutator):
↓ LOCK_EX /var/lib/montana-vpn-balance/balances.json.lock
↓ load balances.json
↓ credit address × elapsed × 0.001
↓ save balances.json atomic
↓ LOCK_UN
↓ jsonify response → возврат
nginx → backend exit (NewYork) → Helsinki → клиент
↓ org.json.JSONObject в Kotlin
↓ MontanaVpnService.lastBalance, .lastStatusText, .connectedNode
WebView UI tick() показывает новый баланс
Полный round-trip ~500-1500 ms в зависимости от exit-узла и сетевых условий.