Перейти к основному содержимому

Протокол рукопожатия и обновления ключей

Статус: Обновляемый документ Последнее обновление: 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 обнаруживает нагрузку.

Значение 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 / MAC216 байтВыход BLAKE2s-128
Интервал cookie120 секундОкно действия cookie, привязанного к IP
Размер ответа cookie56 байтnonce (24) + зашифрованный cookie (16) + tag (16)
Длина AAD60 байтsessionId (32) + direction (16) + nonce (12)
UDP route-id8 bytessession identifier prefix for O(1) peer lookup
Счётчик nonce80 битСообщений на эпоху до переполнения
Окно повтора1024 битаДопуск нарушения порядка UDP
Ёмкость эпохuint1665535 значений, безопасный порог 65000
Интервал обновления ключей120 секундТриггер периодического обновления ключей по умолчанию
Таймаут ожидания5 секундАвтоматическое прерывание неподтверждённого обновления