138 lines
7.4 KiB
Markdown
138 lines
7.4 KiB
Markdown
|
|
# Single Source of Truth — VPN-сеть Montana
|
|||
|
|
|
|||
|
|
Версия: **2026-05-19**
|
|||
|
|
|
|||
|
|
## Принцип
|
|||
|
|
|
|||
|
|
Вся канонически верифицируемая правда о состоянии VPN-сети живёт в **одном append-only signed event log**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
https://montana.quest/vpn/events.jsonl
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Каждая строка — событие, индивидуально подписанное **ed25519**:
|
|||
|
|
|
|||
|
|
```jsonl
|
|||
|
|
{"data":{...},"seq":N,"sig":"ed25519:...","ts":"2026-05-19T19:00:00Z","type":"node_register"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Свойства:**
|
|||
|
|
- **Append-only:** запись только в конец, прошлые события не переписываются.
|
|||
|
|
- **Подписанные:** каждое событие проверяется публичным ключом `d9a8bf07871d35c8e85f7de4a9b62896c330ba0987732468515c7bda8bb4adde`.
|
|||
|
|
- **Монотонный seq:** счётчик никогда не убывает, гэпы запрещены.
|
|||
|
|
- **Pure-function state:** state = replay(events).
|
|||
|
|
|
|||
|
|
## Типы событий
|
|||
|
|
|
|||
|
|
| type | data | когда |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `node_register` | `{alias, ip, country, label}` | admin регистрирует узел через /vpn/node/register |
|
|||
|
|
| `node_deregister` | `{alias, ip}` | узел остановлен (xray ExecStopPost) |
|
|||
|
|
| `node_online` | `{alias, ip}` | orchestrator watchdog: probe OK после offline |
|
|||
|
|
| `node_offline` | `{alias, ip}` | orchestrator watchdog: 2 probe fails подряд |
|
|||
|
|
| `unique_user` | `{ip_hash}` | новый sha256(client_IP) в логе xray |
|
|||
|
|
| `metric` | произвольное | периодические метрики (нагрузка, throughput) |
|
|||
|
|
| `checkpoint` | `{seq_range, merkle}` | каждые 1000 событий (план) |
|
|||
|
|
|
|||
|
|
## Derived state
|
|||
|
|
|
|||
|
|
`https://montana.quest/vpn/state.json` — кешированный результат `build_state(events)`. Любой может перепостроить и сравнить.
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"head_seq": 1502,
|
|||
|
|
"head_ts": "2026-05-19T19:25:30Z",
|
|||
|
|
"built_at": "...",
|
|||
|
|
"nodes": {
|
|||
|
|
"helsinki": {"online": true, "ip": "91.132.142.42", "country": "FI", ...},
|
|||
|
|
"frankfurt": {"online": true, ...},
|
|||
|
|
"newyork": {"online": true, ...}
|
|||
|
|
},
|
|||
|
|
"unique_users": 957,
|
|||
|
|
"unique_users_merkle": "...",
|
|||
|
|
"pubkey_hex": "d9a8bf07...",
|
|||
|
|
"event_log_url": "https://montana.quest/vpn/events.jsonl"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`state.json` rebuilds каждые 30 секунд через `montana-state-builder.timer` на Moscow.
|
|||
|
|
|
|||
|
|
## Что НЕ публикуется
|
|||
|
|
|
|||
|
|
- IP-адреса пользователей (это PII). В события `unique_user` идёт **только sha256 IP** — не обратимо без brute-force на 32-битное IPv4 пространство (~4 сек на rainbow tabe).
|
|||
|
|
- Чтобы усложнить brute-force, в roadmap — добавить **secret salt** с публикацией с задержкой 7 дней.
|
|||
|
|
|
|||
|
|
## Архитектура emitters
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────┐
|
|||
|
|
│ /opt/montana-vpn-stats │
|
|||
|
|
│ /event_log.py │
|
|||
|
|
│ (sign + append) │
|
|||
|
|
└────────────┬────────────┘
|
|||
|
|
│
|
|||
|
|
┌────────────────────────────┼────────────────────────────┐
|
|||
|
|
│ │ │
|
|||
|
|
orchestrator watchdog xray-stats collector admin /register
|
|||
|
|
(online/offline transitions) (new unique IPs) (manual events)
|
|||
|
|
│ │ │
|
|||
|
|
└────────────────────────────▼────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
/var/www/montana_quest/vpn/events.jsonl
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
montana-state-builder.timer (30s)
|
|||
|
|
│
|
|||
|
|
/var/www/montana_quest/vpn/state.json
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
сайт + /vpn/sub + /vpn/node/uptime (читают state.json)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Гарантии
|
|||
|
|
|
|||
|
|
| Гарантия | Как доказывается |
|
|||
|
|
|---|---|
|
|||
|
|
| Число уникальных пользователей не подделано | каждое событие `unique_user` подписано; state.json содержит merkle root; replay воспроизводит число |
|
|||
|
|
| Узел не «фейково онлайн» | переходы online↔offline — отдельные подписанные события с timestamp |
|
|||
|
|
| История не переписана | append-only; seq монотонный без гэпов; любой имеет полный лог |
|
|||
|
|
| Нельзя backdate | timestamp на каждом событии; запланировано daily anchoring через OpenTimestamps |
|
|||
|
|
|
|||
|
|
## Что НЕ гарантируется (пока)
|
|||
|
|
|
|||
|
|
- **Forced inclusion:** мы можем замедлить добавление события в лог. Защита: автоматический rebuild каждые 30 секунд.
|
|||
|
|
- **Privatekey compromise:** если ключ утечёт, можно подделать события. Защита: privateKey 600 root на Moscow.
|
|||
|
|
- **Tampering архивных событий:** теоретически root доступом на Moscow можно переписать `events.jsonl`. **Защита roadmap:** ежедневный snapshot хеша всего лога в Bitcoin через OpenTimestamps — нельзя backdate.
|
|||
|
|
|
|||
|
|
## Аудит
|
|||
|
|
|
|||
|
|
Любой может проверить single-source-of-truth:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
bash Montana/Node/External-Audit/scripts/verify-state.sh
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Этот скрипт:
|
|||
|
|
1. Скачивает `events.jsonl` и `state.json`.
|
|||
|
|
2. Проверяет ed25519-подпись каждого события.
|
|||
|
|
3. Проверяет монотонность seq без гэпов.
|
|||
|
|
4. Replay events до `head_seq` из state.json.
|
|||
|
|
5. Сравнивает результат replay со state.json: `head_seq`, `unique_users`, `merkle root`, `nodes online/offline`.
|
|||
|
|
|
|||
|
|
Если хотя бы один тест провалится — мы лжём.
|
|||
|
|
|
|||
|
|
## Сейчас зелёные тесты
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
✓ все 1531 events подписаны валидно, seq непрерывен
|
|||
|
|
✓ replay matches claimed state: head_seq=1502, nodes=3, unique_users=957
|
|||
|
|
=== ВСЕ ПРОВЕРКИ ЗЕЛЁНЫЕ ===
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Roadmap
|
|||
|
|
|
|||
|
|
- **P1:** daily checkpoint в Bitcoin через OpenTimestamps. Дописать `transparency.md`.
|
|||
|
|
- **P2:** мульти-эмиттер — каждый узел независимо подписывает свои события + threshold-signature (2 of 3). Устраняет single trust на Moscow privateKey.
|
|||
|
|
- **P3:** GraphQL-like derivation API — клиенты запрашивают subset state через `?fields=...`.
|
|||
|
|
- **P4:** Public mirror лога на IPFS/GitHub raw — анти-цензура.
|