montana/Android/Внешний-аудит/приложения/api-эндпоинты.md
2026-05-18 22:11:45 +03:00

6.9 KiB
Raw Blame History

Приложение В. 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 не JSON
  • 400 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-узла и сетевых условий.