Протокол рукопожатия и обновления ключей
Статус: Обновляемый документ Последнее обновление: 2026-02-17
Обзор
TunGo использует рукопожатие Noise IK для взаимной аутентификации и согласования ключей, с последующим периодическим обновлением ключей через X25519 + HKDF-SHA256. Транспортное шифрование использует ChaCha20-Poly1305 AEAD с управлением nonce на основе эпох.
Набор шифров: X25519 / ChaChaPoly / SHA-256
Идентификатор протокола: "TunGo", версия 0x01
1. Рукопожатие (Noise IK)
Noise IK предполагает, что инициатор (клиент) уже знает статический открытый ключ ответчика (сервера).
1.1 Поток сообщений
Client Server
│ │
│─── MSG1: (e, es, s, ss) + MAC1 + MAC2 ───────>│
│ │
│<── COOKIE REPLY (optional, under load) ───────│
│ │
│─── MSG1 (retry with cookie) ─────────────────>│
│ │
│<── MSG2: (e, ee, se) ─────────────────────────│
│ │
├═══ Transport keys established ════════════════╡
1.2 MSG1 (Клиент -> Сервер)
Формат на проводе:
[1B version] [>=80B noise_payload] [16B MAC1] [16B MAC2]
- version:
0x01 - noise_payload: Первое сообщение Noise IK — эфемерный открытый ключ клиента (32 байта, открытым текстом) + зашифрованный статический ключ клиента (48 байт)
- MAC1: Аутентификация без сохранения состояния (проверяется всегда)
- MAC2: Аутентификация на основе cookie (проверяется только при нагрузке)
Минимальный размер: 113 байт.
1.3 MSG2 (Сервер -> Клиент)
Второе сообщение Noise IK. Без MAC — двусторонняя аутентификация неявно обеспечивается после завершения Noise.
После MSG2 обе стороны вычисляют:
c2sKey(32 байта) — транспортный ключ «клиент → сервер»s2cKey(32 байта) — транспортный ключ «сервер → клиент»sessionId(32 байта) — из привязки канала Noise
1.4 Порядок проверки на сервере
1. CheckVersion() — reject unknown protocol versions
2. VerifyMAC1() — stateless, before any DH or allocation
3. VerifyMAC2() — only under load (LoadMonitor)
4. Noise handshake — DH computations, peer lookup
5. Peer ACL check — AllowedPeers / PeerDisabled
Все ошибки возвращают единообразное ErrHandshakeFailed для предотвращения утечки информации.
2. Защита от DoS-атак
2.1 MAC1 (Без сохранения состояния, всегда обязателен)
key = BLAKE2s-256("mac1" || "TunGo" || 0x01 || server_pubkey)
MAC1 = BLAKE2s-128(key, noise_msg1)
Проверяется до выделения любого состояния или вычислений DH.
2.2 MAC2 (С сохранением состояния, при нагрузке)
key = BLAKE2s-256("mac2" || "TunGo" || 0x01 || cookie_value)
MAC2 = BLAKE2s-128(key, noise_msg1 || MAC1)
Проверяется только когда LoadMonitor обнаруживает нагрузку.
2.3 Механизм Cookie
Значение cookie (привязано к IP, разбито на временные интервалы):
bucket = unix_seconds / 120
cookie = BLAKE2s-128(server_secret[32], client_ip[16] || bucket[2])
Действительно для текущего и предыдущего интервала (обработка переходных моментов).
Ответ cookie (зашифрованный, 56 байт):
[24B nonce] [16B encrypted_cookie] [16B poly1305_tag]
Шифрование:
key = BLAKE2s-256("cookie" || "TunGo" || 0x01 || server_pubkey || client_ephemeral)
ciphertext = XChaCha20-Poly1305.Seal(key, nonce, cookie, aad=client_ephemeral)
3. Транспортное шифрование
3.1 AEAD
ChaCha20-Poly1305 с 60-байтным AAD:
AAD [60 bytes]:
[ 0..31] sessionId (32 bytes)
[32..47] direction (16 bytes: "client-to-server" or "server-to-client")
[48..59] nonce (12 bytes)
SessionId и direction предзаполняются при создании сессии. Для каждого пакета обновляется только nonce.
3.2 Структура Nonce (12 байт)
[0..7] counterLow (uint64, big-endian)
[8..9] counterHigh (uint16, big-endian)
[10..11] epoch (uint16, big-endian)
- Счётчик: 80-битный монотонный (2^80 сообщений на эпоху). При переполнении возвращается ошибка.
- Эпоха: Неизменяема в рамках сессии, идентифицирует поколение обновления ключей.
3.3 Транспорт TCP
Wire frame: [2B epoch] [ciphertext + 16B tag]
- Двойная эпоха: текущая и предыдущая сессии сосуществуют во время обновления ключей.
- Автоочистка: предыдущая сессия обнуляется при первом дешифровании с текущей эпохой (гарантия порядка TCP).
- Без защиты от повтора (TCP обеспечивает порядок).
3.4 Транспорт UDP
Wire frame: [8B route-id] [12B nonce] [ciphertext + 16B tag]
-
Route-id is derived from
sessionId(first 8 bytes, big-endian) and enables O(1) session lookup. -
Эпоха встроена в байты nonce 10..11.
-
Защита от повтора: 1024-битное скользящее окно (bitmap) для каждой эпохи.
- Предварительная проверка перед дешифрованием (Check).
- Фиксация только после успешной аутентификации AEAD (Accept).
- Предотвращает отравление окна недействительными пакетами.
-
Кольцо эпох: FIFO фиксированной ёмкости для сессий. Вытесненные сессии обнуляются.
4. Обновление ключей
4.1 Выведение ключей
Обе стороны выполняют X25519 ECDH, затем выводят новые транспортные ключи через HKDF-SHA256:
shared = X25519(local_private, remote_public)
newC2S = HKDF-SHA256(ikm=shared, salt=currentC2S, info="tungo-rekey-c2s")
newS2C = HKDF-SHA256(ikm=shared, salt=currentS2C, info="tungo-rekey-s2c")
Текущие ключи используются как соль HKDF, обеспечивая цепочку прямой секретности.
4.2 Пакеты управляющей плоскости
RekeyInit: [0xFF] [0x01] [0x02] [32B X25519 public key] (35 bytes)
RekeyAck: [0xFF] [0x01] [0x03] [32B X25519 public key] (35 bytes)
4.3 Конечный автомат обновления ключей
StartRekey installPending
Stable ──────────> Rekeying ──────────────> Pending
^ │
│ ActivateSendEpoch │
└───────────────────────────────────────────┘
^ │
│ AbortPendingIfExpired (5s) │
└───────────────────────────────────────────┘
| Состояние | Описание |
|---|---|
| Stable | Нормальная работа. Одна активная эпоха отправки. |
| Rekeying | Вызван StartRekey, новые ключи вычислены, новая эпоха установлена для приёма. |
| Pending | Ожидание подтверждения от пира (первое успешное дешифрование с новой эпохой). |
4.4 Процесс обновления ключей
Client Server
│ │
│── RekeyInit (client X25519 pub) ────────>│
│ │ derive newC2S, newS2C
│ │ install new epoch (recv)
│<── RekeyAck (server X25519 pub) ─────────│
│ │
│ derive newC2S, newS2C │
│ install new epoch (recv + send) │
│ │
│── first packet with new epoch ──────────>│
│ │ peer confirmed → activate send
│<── first packet with new epoch ──────────│
│ │
├═══ Both sides on new epoch ══════════════╡
4.5 Инварианты безопасности
- Только одно обновление ключей в процессе одновременно.
- Эпохи монотонно возрастают. Максимальная безопасная эпоха: 65000 (из 65535). При превышении
ErrEpochExhaustedвынуждает повторное рукопожатие. - Эпоха отправки никогда не уменьшается.
- Ожидающие ключи никогда не перезаписывают активные, пока пир не докажет владение (через успешное дешифрование).
- Ожидающее обновление ключей автоматически прерывается через 5 секунд при отсутствии подтверждения от пира.
- Интервал обновления ключей по умолчанию: 120 секунд.
5. Обнуление ключей
| Материал | Момент обнуления |
|---|---|
| Эфемерные закрытые ключи DH | Сразу после вычисления DH (defer mem.ZeroBytes) |
| Общие секреты (обновление ключей) | Сразу после выведения ключей (defer mem.ZeroBytes) |
| Ожидающие ключи обновления (конечный автомат) | При прерывании или повышении до активных |
| Ключи предыдущей сессии | При первом дешифровании с текущей эпохой (TCP) или вытеснении эпохи (UDP) |
| Окно повтора nonce | При завершении сессии (SlidingWindow.Zeroize) |
| Буферы AAD | При завершении сессии (DefaultUdpSession.Zeroize) |
Ограничение: Сборщик мусора Go может копировать объекты в куче до обнуления. mem.ZeroBytes — защита по мере возможности от криминалистического анализа памяти, верифицировано анализом выхода компилятора на предмет отсутствия оптимизации (Go 1.26.x, все целевые платформы).
6. Константы
| Константа | Значение | Назначение |
|---|---|---|
| Версия протокола | 0x01 | Версионирование формата на проводе |
| Размер MAC1 / MAC2 | 16 байт | Выход BLAKE2s-128 |
| Интервал cookie | 120 секунд | Окно действия cookie, привязанного к IP |
| Размер ответа cookie | 56 байт | nonce (24) + зашифрованный cookie (16) + tag (16) |
| Длина AAD | 60 байт | sessionId (32) + direction (16) + nonce (12) |
| UDP route-id | 8 bytes | session identifier prefix for O(1) peer lookup |
| Счётчик nonce | 80 бит | Сообщений на эпоху до переполнения |
| Окно повтора | 1024 бита | Допуск нарушения порядка UDP |
| Ёмкость эпох | uint16 | 65535 значений, безопасный порог 65000 |
| Интервал обновления ключей | 120 секунд | Триггер периодического обновления ключей по умолчанию |
| Таймаут ожидания | 5 секунд | Автоматическое прерывание неподтверждённого обновления |