todo/amnezia-3-0/internals.md
loop-uh 36c2fb575b
Some checks failed
Published content check / validate (push) Failing after 6s
AmneziaWG 3.x: снятие параметров только пересозданием интерфейса, dynamic debug, статус клиентов
По материалам мейнтейнера amneziawg-installer (проверено по коду и GitHub API):
- internals: AWG-параметр нельзя убрать через awg setconf/syncconf — обе
  реализации обрабатывают только присутствующие ключи; команда включения
  dynamic debug для пояснений к отказам модуля ядра;
- reference: поддержка AWG 3.0 в mihomo v1.19.30 (16 августа), статусы
  официальных клиентов amneziawg-android (3.x — только предрелизы) и
  amneziawg-windows-client (стабильный 2.0.2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 20:19:39 +03:00

309 lines
53 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
date: 2026-08-04
tags:
- amneziawg
- wireguard
- обфускация
- dpi
- reverse-engineering
aliases:
- AmneziaWG 3.0 внутреннее устройство
- AWG 3.0 wire-format
- Header protection AmneziaWG
- Как устроена Амнезия 3.0 изнутри
- HeaderProtectionKey что это и как сгенерировать
- Почему S1-S4 должны быть не меньше 12
- ContentPaddingAddition в AmneziaWG
- Амнезия ВПН шифрование заголовков
- AmneziaWG рукопожатие каждые 15 секунд
- unknown tag c AmneziaWG
link: https://github.com/amnezia-vpn/amneziawg-go/tree/v3.0.3
---
# 🔬 AmneziaWG 3.0: внутреннее устройство протокола
> [!info] О чём заметка
> Побайтовый разбор третьего поколения AmneziaWG по исходному коду: как собирается пакет, что именно шифрует защита заголовков, откуда берётся одноразовое число, как приёмная сторона опознаёт тип сообщения, не расшифровав его целиком, и какие следы протокол всё ещё оставляет в сети. Обзорная часть — назначение, доступность, история версий — в заметке [[amnezia-3-0/reference|AmneziaWG 3.0]]; параметры предыдущего поколения (`Jc`, `S1``S4`, `H1``H4`, язык CPS) подробно разобраны в [[amnezia-2-0/reference|справочнике по AmneziaWG 2.0]] и здесь не повторяются.
## TL;DR
- Третье поколение добавляет к обфускации ровно три механизма: **защита заголовков** (ChaCha20 поверх готовых сообщений WireGuard), **content padding** (случайное удлинение полезной нагрузки вместо выравнивания по 16 байт) и **диапазонные тайминги** (новое случайное значение при каждом взводе таймера).
- Одноразовое число (nonce) для шифра **не передаётся отдельно** — им служат первые 12 байт того самого случайного паддинга `S1``S4`, который в 2.0 был просто мусором. Отсюда требование `S1``S4` ≥ 12 при включённой защите.
- Приёмная сторона использует изящный приём: шифрует блок нулей, получая первые 4 байта потока ключа, и этим «хэшем типа» расшифровывает только поле типа — чтобы опознать сообщение до разбора остального пакета.
- **Тихое улучшение, не отмеченное нигде в документации**: keepalive-пакеты теперь получают и паддинг `S4`, и content padding. В [[amnezia-2-0/reference|AWG 2.0]] они шли без `S4` и имели постоянный размер 32 байта — это была заметная сигнатура. У улучшения оказался побочный эффект: до исправления 5 августа 2026 года западдженный keepalive засчитывался как пакет с данными, и простаивающий туннель делал рукопожатие каждые ~15 секунд — примерно в десять раз чаще нормы; разбор ниже.
- **Что 3.0 не закрывает**: размеры рукопожатий остаются постоянными для конкретной конфигурации (`S1`+148 и `S2`+92 байта), потому что content padding применяется только к транспортным пакетам. Пара «фиксированный запрос → фиксированный ответ → поток» по-прежнему видна наблюдателю. Именно этот признак закрывает линия 3.1 от 12 августа 2026 года параметром `RandomTrailers` — см. [[amnezia-3-0/reference|обзорную заметку]].
## Как проверялись факты
Всё ниже прочитано в исходниках движка [amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go) на теге **`v3.0.3`** (коммит `cf9d2dd`, 31 июля 2026 года) — это последняя на 4 августа 2026 года версия третьего поколения. Ключевые файлы: `device/noise-types.go` (константы и тип диапазона), `device/noise-protocol.go` (выдача шифра), `device/send.go` (сборка исходящих пакетов), `device/receive.go` (разбор входящих), `device/uapi.go` (параметры и их проверка), `device/timers.go` (тайминги).
После 4 августа линия получила продолжение: 5 августа 2026 года в go и в модуле ядра вышли теги `v3.0.20260805` с исправлением keepalive-дефекта (разобран ниже), а 12 августа — линия 3.1 с параметрами `RandomTrailers` и `DisableCookies`, обзор которой — в [[amnezia-3-0/reference|заметке про AmneziaWG 3.0]]. Описанную здесь механику 3.0 эти теги не меняют: формат пакетов и разбор тегов CPS в 3.1 идентичны 3.0 (сверено диффом 20 августа 2026 года). Раздел о наборах тегов сигнатур дополнительно сверен с модулем ядра (`src/junk.c`, теги `v3.0.20260805` и `v3.1.20260812`).
> [!warning] Первоисточник здесь — код, а не документация
> Официального описания механики третьего поколения не существует: к 20 августа 2026 года страница docs.amnezia.org об AmneziaWG обзавелась таблицей параметров вплоть до 3.0, но с оговоркой, что подробности добавят после выхода self-hosted-поддержки; обещанная статья в блоге так и не вышла. README репозитория покрывает список параметров, но не механику. Поэтому разбор ниже — чтение исходников, и любые расхождения с будущей официальной документацией следует трактовать в её пользу. Отдельно предупреждение о ходящем по сети «разборе под капотом» из GitHub Discussions: значительная часть его утверждений кодом не подтверждается, подробности — в [[amnezia-3-0/reference|обзорной заметке]].
## Полный список параметров устройства
Третье поколение AmneziaWG (*AWG 3, «амнезия 3.0», «АмнезияВГ 3»*) не переизобретает конфигурацию, а дописывает к ней восемь новых ключей. Полная картина того, что понимает движок `v3.0.3` (имена в конфигурационном файле и соответствующие им ключи внутреннего интерфейса UAPI, через который утилиты общаются с движком):
| В конфиге | Ключ UAPI | Тип | Появился | Сторона |
|---|---|---|---|---|
| `Jc`, `Jmin`, `Jmax` | `jc`, `jmin`, `jmax` | int | 1.0 | клиентская |
| `S1``S4` | `s1``s4` | int | 1.0 (`S3`, `S4` — в 2.0) | серверная |
| `H1``H4` | `h1``h4` | диапазон uint32 | 1.0 (диапазоны — в 2.0) | серверная |
| `I1``I5` | `i1``i5` | строка на языке CPS | 1.5 | клиентская |
| **`HeaderProtectionKey`** | `header_protection_key` | 32 байта, base64 | **3.0** | серверная |
| **`ContentPaddingAddition`** | `content_padding_addition` | диапазон uint32 | **3.0** | клиентская |
| **`RekeyAfterTime`** | `rekey_after_time` | диапазон uint32, секунды | **3.0** | клиентская |
| **`RekeyTimeout`** | `rekey_timeout` | диапазон uint32, секунды | **3.0** | клиентская |
| **`RejectAfterTime`** | `reject_after_time` | диапазон uint32, секунды | **3.0** | клиентская |
| **`KeepaliveTimeout`** | `keepalive_timeout` | диапазон uint32, секунды | **3.0** | клиентская |
| **`MaxHandshakeAttempts`** | `max_handshake_attempts` | диапазон uint32, попытки | **3.0** | клиентская |
| `PersistentKeepalive` | `persistent_keepalive_interval` | стал диапазоном | изменён в **3.0** | клиентская |
Разделение на «стороны» в терминологии README означает буквально следующее: **серверные** параметры обязаны совпадать на обоих концах туннеля, иначе стороны не поймут друг друга; **клиентские** можно задавать только на одной стороне — они влияют на то, как эта сторона отправляет, и не требуют согласования.
Проще говоря: ключ защиты заголовков и значения паддинга должны быть одинаковыми у клиента и сервера, а мусорные пакеты, content padding и тайминги каждый настраивает под себя.
Формат диапазона — `a-b`, либо одиночное число (тогда границы совпадают), либо `(off)`. Разбирается он в тип `UintRange`, где обе границы упакованы в одно 64-битное число: младшие 32 бита — нижняя граница, старшие — верхняя. Такая упаковка позволяет менять диапазон атомарно, без блокировок, прямо во время работы туннеля.
## Слои обфускации: как собирается исходящий пакет
Полезно держать в голове порядок, в котором данные обрастают слоями. Для транспортного пакета он такой:
1. **Прикладные данные** приходят из виртуального сетевого интерфейса.
2. **Content padding** — в хвост дописываются нулевые байты: случайное количество из диапазона `ContentPaddingAddition`, а если параметр не задан — ровно столько, чтобы длина стала кратна 16 (штатное поведение WireGuard).
3. **Шифрование полезной нагрузки** — ChaCha20-Poly1305 сеансовым ключом, штатный механизм WireGuard. На выходе получается шифртекст плюс 16-байтовая метка подлинности.
4. **Заголовок WireGuard** — 16 байт: тип сообщения (значение из диапазона `H4`), индекс получателя, счётчик пакетов.
5. **Криптопаддинг `S4`** — перед заголовком в буфере лежат `S4` случайных байт.
6. **Защита заголовков** — 16 байт заголовка шифруются ChaCha20 с одноразовым числом из первых 12 байт паддинга.
Ключевая перестановка относительно [[amnezia-2-0/reference|второго поколения]] — в шаге 5. Раньше паддинг `S4` дописывался в самом конце, сдвигом уже готового зашифрованного пакета вправо по буферу. Теперь место под него резервируется заранее (`elem.padding` выставляется при создании исходящего элемента), и заполняется он до шифрования заголовка — иначе неоткуда было бы взять одноразовое число.
Для рукопожатия порядок другой и полностью сохраняет логику 2.0: сначала уходят сигнатурные пакеты `I1``I5` (каждый — отдельная датаграмма), затем `Jc` мусорных пакетов, и лишь потом само сообщение инициации. Всё это отправляется одним системным вызовом.
## Header protection побайтово
### Ключ
Параметр `HeaderProtectionKey` — 32 байта, в конфигурационном файле записывается в base64, как обычный ключ WireGuard; шестнадцатеричная форма используется только на внутреннем интерфейсе UAPI. Генерируется командой `awg genkey`.
Ключ хранится в устройстве под отдельной блокировкой чтения-записи и **живёт всё время работы интерфейса**: он не участвует в выработке сеансовых ключей и не меняется при обновлении сессии каждые пару минут. Это осознанный размен — заголовки нужно уметь расшифровать до того, как станет понятно, к какой сессии относится пакет.
Если ключ нулевой, функция выдачи шифра возвращает пустое значение, и весь механизм отключается — движок ведёт себя ровно как AWG 2.0. Именно поэтому «несовместимость с 2.0» на самом деле означает «несовместимость конфигураций с включённой защитой заголовков»: сам движок умеет работать в обоих режимах.
### Одноразовое число
Шифр — ChaCha20 в варианте IETF, без аутентификации (`chacha20.NewUnauthenticatedCipher`). Его одноразовое число занимает 12 байт, и берётся оно не из отдельного поля, а **из начала криптопаддинга**:
```
Отправка (сообщение инициации):
buf = [ S1 байт ] ← crypto/rand.Read по всей длине
[ 148 байт сообщения ]
nonce = buf[:12] ← первые 12 байт паддинга
cip = ChaCha20(key, nonce)
cip.XORKeyStream(packet, packet) ← шифруется всё сообщение целиком
```
Отсюда единственное жёсткое ограничение третьего поколения: при непустом ключе **все четыре значения `S1``S4` должны быть не меньше 12**, иначе одноразовое число просто не поместится. Проверка стоит в обработчике настроек и отвергает всю конфигурацию целиком.
Обратите внимание на побочный эффект: `S4` ≥ 12 означает, что 12 с лишним лишних байт добавляются **к каждому транспортному пакету**, а не только к рукопожатиям. Отключить паддинг для потока данных, сохранив защиту заголовков, нельзя.
### Что именно шифруется
| Тип сообщения | Размер | Что покрывает шифр |
|---|---|---|
| Инициация рукопожатия | 148 байт | всё сообщение, включая MAC1 и MAC2 |
| Ответ на рукопожатие | 92 байта | всё сообщение целиком |
| Ответ с cookie | 64 байта | всё сообщение целиком |
| Транспортный пакет | 16 байт заголовка | только заголовок: тип, индекс получателя, счётчик |
Полезная нагрузка транспортных пакетов вторым слоем не шифруется, и это правильно: она уже зашифрована ChaCha20-Poly1305 и статистически неотличима от случайных данных, так что второй проход дал бы нулевой выигрыш в маскировке при заметной трате процессора.
Проще говоря: третье поколение прячет не содержимое — оно и так было спрятано, — а **служебную разметку**, по которой пакет опознавался как WireGuard.
### Приём: трюк с «хэшем типа»
Здесь самая любопытная часть реализации. Проблема очевидна: чтобы расшифровать заголовок, нужно знать длину паддинга, а она зависит от типа сообщения, который сам лежит в зашифрованном заголовке. Замкнутый круг.
Решение опирается на свойство потокового шифра — шифрование блока нулей даёт чистый поток ключа:
```
Приём (любой пакет):
nonce = packet[:12] ← первые 12 байт датаграммы
cip = ChaCha20(key, nonce)
typeHash = cip.XORKeyStream(0x00000000) ← первые 4 байта потока ключа
для каждого кандидата (init / response / cookie / transport):
если размер пакета == S_i + размер_сообщения:
тип = packet[S_i : S_i+4] XOR typeHash ← расшифровка только поля типа
если тип попадает в диапазон H_i → это оно
далее поток ключа продолжается с 5-го байта:
cip.XORKeyStream(packet[4:конец_заголовка])
```
Такая конструкция даёт две вещи. Во-первых, приёмник расшифровывает четыре байта вместо целого пакета и только потом решает, стоит ли возиться дальше. Во-вторых, поток ключа расходуется строго последовательно и в точности повторяет порядок, в котором шифровала отправляющая сторона: первые 4 байта ушли на поле типа, остальное — на всё, что за ним.
Отдельная приятная деталь: при выключенной защите заголовков «хэш типа» состоит из нулей, а операция «исключающее ИЛИ» с нулями ничего не меняет. Один и тот же код работает в обоих режимах без ветвлений — источник целого класса ошибок здесь просто отсутствует.
## Как выглядит пакет на проводе
**Транспортный пакет (третье поколение, защита включена):**
```
┌───────────┬────────────────┬──────────────────────┬───────────────────────────┬───────────┐
│ nonce │ остаток S4 │ заголовок (16 байт) │ шифртекст полезной │ метка │
│ 12 байт │ S4-12 байт │ ЗАШИФРОВАН ChaCha20 │ нагрузки + content padding│ Poly1305 │
│ случайных │ случайных │ тип│получатель│счётчик│ │ 16 байт │
└───────────┴────────────────┴──────────────────────┴───────────────────────────┴───────────┘
└── открытым текстом, но неотличимо от шума ──┘ └── и до, и после: сплошная псевдослучайность ──┘
```
**Сообщение инициации рукопожатия:**
```
┌───────────┬───────────────┬──────────────────────────────────────────────┐
│ nonce │ остаток S1 │ 148 байт сообщения, ЗАШИФРОВАННЫХ ЦЕЛИКОМ │
│ 12 байт │ S1-12 байт │ (тип, отправитель, ключи, метка времени, │
│ │ │ MAC1, MAC2 — всё) │
└───────────┴───────────────┴──────────────────────────────────────────────┘
Итоговый размер: S1 + 148 байт — постоянный для данной конфигурации
```
Для наблюдателя со стороны сети датаграмма целиком выглядит равномерным шумом: ни одного поля с предсказуемым значением, ни одной структуры, за которую можно зацепиться сигнатурой. В версии 2.0 первые четыре байта после паддинга были осмысленным числом из диапазона `H1``H4`, а дальше шли постоянный индекс получателя и монотонно растущий счётчик.
## Content padding: как считается добавка
Механизм устроен проще, чем можно подумать по названию, и умещается в один вспомогательный расчёт:
- если `ContentPaddingAddition` не задан, возвращается признак «нет добавки», и работает обычное выравнивание длины до кратности 16;
- если задан, из диапазона берётся случайное число;
- добавка ограничивается сверху свободным местом до MTU (максимального размера передаваемого блока), чтобы не спровоцировать фрагментацию;
- полученное количество **нулевых** байт дописывается в конец открытого текста, после чего всё вместе шифруется.
Два следствия, важных на практике. Первое: добавка попадает **внутрь** зашифрованной части, наблюдатель видит только изменившуюся длину пакета — сами байты паддинга он отличить от данных не может. Второе: заданный content padding **отменяет** штатное выравнивание по 16 байт. Именно в этом смысл механизма — предсказуемая сетка длин, кратных шестнадцати, сама по себе является признаком, по которому поток можно отнести к WireGuard-подобным.
> [!note] Keepalive тоже паддится — это скрытое улучшение
> Служебные пакеты поддержания соединения проходят тот же путь, что и обычные: паддинг `S4` им назначается при создании исходящего элемента, а content padding считается от нулевого размера полезной нагрузки. В [[amnezia-2-0/reference|версии 2.0]] keepalive шёл в обход `S4` и всегда весил ровно 32 байта — постоянный размер, повторяющийся строго по таймеру, то есть отличный опознавательный признак. Ни в README, ни в анонсах это изменение не упомянуто.
### Побочный эффект: рукопожатие каждые 15 секунд вместо двух минут
У паддинга keepalive обнаружился дефект, живший в релизах третьего поколения с 24 июля по 5 августа 2026 года. Обе реализации опознавали keepalive по длине пакета, а паддинг эту длину изменил. В `amneziawg-go` тега `v3.0.3` проверка «отправлены ли данные» сравнивает длину уже собранного пакета с 32 байтами (`len(elem.packet) != MessageKeepaliveSize` в `device/send.go`), но к этому моменту в пакет уже вшит префикс `S4` — при любом ненулевом `S4` каждый keepalive засчитывается как данные. Второй, независимый путь — content padding: расшифрованный keepalive у приёмника перестаёт быть пустым, проверка `len == 0` в `device/receive.go` не срабатывает, и пакет уходит в ветку «получены данные».
Следствие для простаивающего туннеля: отправка «данных» взводит таймер нового рукопожатия на `KeepaliveTimeout + RekeyTimeout` — по умолчанию 10 + 5 = 15 секунд, — а погасить его на молчащем туннеле нечем. Получается самоподдерживающийся цикл: рукопожатие → подтверждающий keepalive → таймер → новое рукопожатие, и так каждые ~15 секунд. Замеры в issue [#186](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/186) модуля ядра: медиана интервала между рукопожатиями 15,0 с при `S4 = 17` против 147148 с у контроля без `S4` — примерно десятикратный рост. Перед каждым лишним рукопожатием, как обычно, уходят `Jc` мусорных и `I1``I5` сигнатурных пакетов, так что дефект не просто тратил трафик, а умножал самую заметную часть почерка протокола.
Границы дефекта по реализациям различаются. В модуле ядра он старше третьего поколения: issue #186 создано 15 июля 2026 года, за две недели до выхода 3.0 в модуле, — проверка `is_keepalive = skb->len == message_data_len(0)` в `src/send.c` выполнялась после добавления `S4`-мусора, то есть ошибка касалась и установок [[amnezia-2-0/reference|AWG 2.0]] с ненулевым `S4`. В go-движке дефект появился именно в линии 3.0 — на `v0.2.19` та же проверка шла до добавления паддинга, и keepalive распознавался корректно (полевые замеры в том же issue: медиана 15,0 с на `v3.0.3` против 132,0 с на `v0.2.19`).
Исправление: в модуле ядра — PR [#208](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/pull/208) (смержен 5 августа 2026; keepalive теперь помечается явным флагом, а приёмник считает keepalive-ом и пакет из одних нулевых байт), вошёл в теги `v3.0.20260805` и `v3.1.20260812`. В go исправление вышло в тот же день тегом `v3.0.20260805`. На более ранних сборках дефект лечился бы только нулевым `S4`, что при включённой защите заголовков невозможно (`S4` ≥ 12), — поэтому обновление обязательно.
## Тайминги: где берётся случайное значение, а где граница
Шесть таймеров стали диапазонами, но применяются они не одинаково, и разница принципиальна для устойчивости туннеля.
**Случайное значение при каждом взводе** (`PickOne` берёт новое число из диапазона всякий раз): интервал переустановки ключей, пауза перед повтором рукопожатия, таймер отправки keepalive, интервал `PersistentKeepalive`, максимальное число попыток рукопожатия. Именно эти вызовы и размывают ритм соединения — два подряд рукопожатия не совпадут по времени.
**Границы диапазона вместо случайного значения** — там, где протокол обязан сохранять внутренние инварианты. Например, срок жизни связки ключей вычисляется по **верхней** границе, а минимальный интервал между попытками рукопожатия — по **нижней**. Логика понятна: если бы движок брал случайные значения и здесь, он мог бы, например, посчитать ключ протухшим раньше, чем истёк допустимый срок его использования, и туннель начал бы рвать сам себя.
Проще говоря: случайность добавлена там, где она видна снаружи и не ломает протокол, а во внутренних проверках используются осторожные крайние значения.
## Что изменилось относительно AWG 2.0
| Аспект | AWG 2.0 | AWG 3.0 |
|---|---|---|
| Поле типа сообщения | значение из диапазона `H1``H4`, открыто | зашифровано |
| Индекс получателя и счётчик | открыты, предсказуемы | зашифрованы |
| MAC1 и MAC2 в рукопожатиях | открыты | зашифрованы вместе со всем сообщением |
| Паддинг `S1``S4` | чистый мусор | мусор плюс источник одноразового числа |
| Минимум `S1``S4` | 0 (любое) | 12 при включённой защите заголовков |
| Момент добавления `S4` | сдвиг готового пакета вправо | место резервируется заранее |
| Keepalive | без `S4`, ровно 32 байта | с `S4` и content padding |
| Длина полезной нагрузки | выравнивание до 16 байт | случайная добавка из диапазона |
| Тайминги | константы WireGuard | диапазоны, новое значение на каждый взвод |
| Мусорные и сигнатурные пакеты | `Jc`/`Jmin`/`Jmax`, `I1``I5`, язык CPS | без изменений |
| Криптография WireGuard | не менялась | не менялась |
Последнюю строку стоит проверить отдельно, поскольку вокруг неё много домыслов: файл с криптографическими примитивами (`device/noise-helpers.go`) в теге `v3.0.3` **побайтно совпадает** с оригиналом из `wireguard-go`. Рукопожатие Noise IKpsk2, эллиптическая кривая Curve25519, хэш Blake2s — всё нетронуто. Защита заголовков надстроена поверх готовых сообщений и в выработке ключей не участвует.
## Анализ: что протокол закрывает, а что оставляет видимым
Раздел ниже — разбор по коду, а не позиция команды Amnezia: официального описания модели угроз третьего поколения не публиковалось.
### Что закрыто
**Сигнатуры по содержимому пакета.** После шифрования заголовков в датаграмме не остаётся ни одного поля с предсказуемым или медленно меняющимся значением. Правило вида «четыре байта в начале равны 4, следующие четыре постоянны в рамках потока, дальше восьмибайтовый счётчик растёт на единицу» — а это и есть классический способ опознать WireGuard — больше не срабатывает.
**Сигнатура keepalive.** Постоянный 32-байтовый пакет, приходящий строго по таймеру, был удобной зацепкой даже при полностью зашифрованном содержимом: важен не смысл байтов, а сам факт периодического повтора одинакового размера. Теперь размер плавает за счёт `S4` и content padding, а момент отправки — за счёт диапазонного таймера.
**Сетка длин, кратных 16.** Content padding убирает регулярность, которая выдавала внутреннее выравнивание.
### Что осталось
**Постоянные размеры рукопожатий.** Content padding применяется только к транспортным пакетам; сообщения инициации и ответа собираются в буфер фиксированной длины — `S1`+148 и `S2`+92 байта соответственно. Для конкретной конфигурации это две константы, и характерный обмен «датаграмма размера X → в ответ датаграмма размера Y → следом поток» никуда не делся. Универсальной сигнатуры здесь нет, поскольку `S1` и `S2` у каждой установки свои, но **структурный** признак — короткий двухшаговый обмен фиксированных размеров перед началом потока — наблюдаемый. Ровно этот признак закрывает вышедшая 12 августа 2026 года линия 3.1: параметр `RandomTrailers` дописывает к пакетам рукопожатия хвост случайной длины, но требует включения на обеих сторонах — разбор в [[amnezia-3-0/reference|обзорной заметке]].
**Молчание в ответ на активное зондирование.** Пакет, не подошедший ни под один размер и диапазон заголовков, просто отбрасывается без ответа. Для системы, которая проверяет подозрительный адрес отправкой мусора, «сервер, который не отвечает вообще ничем на порт UDP» — тоже поведенческий признак. Механизм отката на локальный веб-сервис, который решал бы эту задачу по образцу REALITY, в репозитории существует, но живёт в экспериментальной ветке и в релиз третьего поколения не вошёл.
**Профиль потока данных.** Объём трафика, ритм пакетов приложения, длительность сессий — обфускация уровня протокола на это не влияет. В описанной разработчиком Amnezia балльной системе, где сервер блокируется по сумме признаков, объём трафика назван одним из ключевых факторов.
**Блокировка по адресу.** Если адрес сервера уже в списках, смена версии протокола не поможет. Это ограничение общее для всех решений такого класса, см. [[DPI/ru-network-blocklists|обзор сетевых блокировок]] и [[DPI/vpn-blocking-wave-forecast-summer-2026|прогноз по волнам блокировок]].
### Криптографические заметки
Не уязвимости, а свойства конструкции, о которых стоит знать.
Ключ защиты заголовков **статичен** на всё время жизни интерфейса, а одноразовое число имеет длину 96 бит и берётся из криптостойкого генератора случайных чисел. Повтор одноразового числа означал бы повторное использование потока ключа — для потокового шифра это ведёт к утечке: исключающее ИЛИ двух заголовков раскрывается. Оценка запаса по «парадоксу дней рождения»: пятидесятипроцентная вероятность совпадения достигается примерно после 2⁴⁸ пакетов — это порядка 10¹⁴ штук, то есть годы непрерывной работы канала на гигабитной скорости. Практического риска нет, но и бесконечным запас не является, а ротации этого ключа в протоколе не предусмотрено.
Шифр применяется **без аутентификации**, и это корректно: подлинность обеспечивают штатные механизмы WireGuard (MAC1 и MAC2 в рукопожатиях, метка Poly1305 в транспортных пакетах), которые остались на месте. Расшифровка заголовка сама по себе ничего не подтверждает — она лишь позволяет опознать пакет, а решение о доверии принимается ниже по конвейеру.
### Накладные расходы
К каждому транспортному пакету добавляется минимум 12 байт паддинга плюс случайная добавка content padding. На типичном пакете в 1400 байт минимальный обязательный оверхед составляет около 0,9% от полосы, что для практических целей несущественно; настроенный на широкий диапазон content padding способен добавить заметно больше — это осознанный размен скорости на маскировку. Вычислительная нагрузка мала: ChaCha20 обрабатывает 16 байт заголовка на пакет, а рукопожатия происходят раз в пару минут.
## Ловушки, замеченные в коде
> [!warning] Сообщение об ошибке нумерует параметры с нуля
> Если движок отвергает конфигурацию, он пишет `S0 must be more then 12 to use headerProtection`. Параметра `S0` не существует: проверка перебирает четыре значения в цикле и подставляет индекс массива, начинающийся с нуля, так что `S0` в сообщении означает **`S1`**, `S1` означает `S2` и так далее. При диагностике прибавляйте единицу. В той же строке живёт опечатка «more then» вместо «more than», а формулировка «больше 12» неточна — проверка отвергает значения **меньше** 12, само число 12 допустимо.
Ещё три момента, на которых легко споткнуться:
**Снять параметр обфускации «на живую» нельзя — только пересозданием интерфейса.** Команды `awg setconf` и `awg syncconf` для AWG-параметров работают на добавление и изменение: обе реализации трогают только те ключи, которые присутствуют в переданной конфигурации, а про отсутствующие ничего не знают — удалённая из файла строка не обнуляет значение на работающем интерфейсе. Изменить значение так можно, убрать совсем — нет: интерфейс придётся пересоздать (`awg-quick down`/`up` или `systemctl restart awg-quick@awg0`), что на сервере оборвёт соединения всех клиентов. Наблюдение мейнтейнера стороннего установщика [amneziawg-installer](https://github.com/bivlked/amneziawg-installer), согласуется с устройством обеих реализаций: и netlink-обработчик модуля ядра, и UAPI-парсер go обрабатывают только присутствующие атрибуты.
**Требование к паддингу распространяется на все четыре значения сразу.** Даже если вы, скажем, не собираетесь получать cookie-ответы под нагрузкой, `S3` всё равно обязан быть не меньше 12 — проверка не смотрит, какие типы сообщений реально используются.
**README до 31 июля 2026 года указывал порог 8.** В коде порог всегда был 12; расходились именно документация и текст ошибки, исправленные коммитом `ce7cf103` в составе тега `v3.0.3`. Конфигурации, собранные по более ранней инструкции, работать не будут.
**Набор тегов сигнатур зависит от реализации.** README перечисляет пять тегов языка CPS, но фактические наборы двух реализаций расходятся в обе стороны (сверено 20 августа 2026 года: `device/obf.go` движка go на тегах `v3.0.3`/`v3.1.20260814`, `src/junk.c` модуля ядра на `v3.0.20260805`/`v3.1.20260812`):
| Тег | amneziawg-go | Модуль ядра |
|---|:--:|:--:|
| `<b>`, `<r>`, `<rc>`, `<rd>`, `<t>` | есть | есть |
| `<d>`, `<ds>`, `<dz>` — недокументированные | есть | **нет** — конфиг отвергается с `-EINVAL` |
| `<c>` — недокументированный счётчик спецпакетов (u32 big-endian, стартует со случайного значения при инициации рукопожатия) | **нет** | есть (`parse_c_tag` в `src/junk.c`) |
Практическое следствие: конфигурация с `<c>` работает на узле с модулем ядра, но не поднимется на userspace-движке — то есть в LXC и Docker, на роутерах, на macOS; конфигурация с `<d>`/`<ds>`/`<dz>` — ровно наоборот. Отказ в обоих случаях жёсткий, но заметен по-разному. Движок go пишет в журнал демона строку вида `failed to parse I1: unknown tag <c>` (клиенту по UAPI уходит только код ошибки). Модуль ядра возвращает голый `-EINVAL`, а поясняющую строку `I1-packet invalid format` печатает через `net_dbg_ratelimited()` — без включённого dynamic debug в `dmesg` пусто; отсюда жалобы вида «интерфейс просто не поднимается, в логе ничего». Правило для рецептов `I1``I5`, которые предлагаются другим людям: использовать только пересечение `{b, r, rc, rd, t}`. Теги `<d>`/`<ds>`/`<dz>` в пакетах `I1``I5` и так почти бесполезны — они получают на вход пустые данные; полный разбор go-набора — в [[amnezia-2-0/reference|справочнике по AmneziaWG 2.0]]. В линиях 3.0 и 3.1 наборы обеих реализаций идентичны, go-набор не менялся с 2.0.
## Практика: как это отлаживать
- [ ] Сгенерировать ключ защиты заголовков: `awg genkey` из пакета `amneziawg-tools` версии `v3.0.20260730` или новее — более старые утилиты не знают новых ключей конфигурации и молча их не передадут.
- [ ] Проверить, что все четыре значения `S1``S4` не меньше 12; при ошибке про `S0` смотреть на `S1`.
- [ ] Убедиться, что `HeaderProtectionKey` побайтно одинаков на сервере и клиенте — при расхождении соединение не установится, а в журнале не будет ничего осмысленного: пакеты просто отбрасываются как неопознанные.
- [ ] Поднять уровень журналирования переменной окружения `LOG_LEVEL=debug` — сообщения о неопознанных пакетах и об инициализации шифра идут именно туда.
- [ ] На модуле ядра пояснения к отказам (`I1-packet invalid format`, `S0 must be more then 12…`) по умолчанию не печатаются: `awg` показывает голое `Unable to modify interface: Invalid argument`. Включить отладку: `echo "module amneziawg +p" > /sys/kernel/debug/dynamic_debug/control`, повторить неудачную команду, посмотреть `dmesg | tail`, затем выключить той же командой с `-p`.
- [ ] На Linux с модулем ядра брать ревизию не ниже `v3.0.20260805`: в ней исправлен keepalive-дефект с рукопожатиями каждые 15 секунд, а в ревизиях до `v3.0.20260731-04` вдобавок падала сборка на ядрах старше 6.7 и мог молча не применяться ключ защиты заголовков.
- [ ] Ревизию загруженного модуля смотреть командой `cat /sys/module/amneziawg/version`: у DKMS-сборки там строка тега (например, `3.1.20260812`), у модуля, собранного вручную через `make`, — `1.0.0`. Версия apt-пакета для проверки не годится: она всегда начинается с `1.0.0` (версия debian-упаковки, заморожена с 2023 года), линия кода видна только по git-хешу в конце строки.
- [ ] Если конфигурация должна работать и с модулем ядра, и с userspace-движком (LXC, Docker, роутеры, macOS) — собирать `I1``I5` только из тегов `{b, r, rc, rd, t}`: наборы тегов реализаций расходятся, подробности в «Ловушках» выше.
- [ ] Не копировать значения `I1``I5` из примеров: повторяющаяся у тысяч установок сигнатура работает против вас, чему посвящён отдельный разбор в [[amnezia-3-0/reference|обзорной заметке]].
## 📚 См. также
- [[amnezia-3-0/reference|AmneziaWG 3.0]] — обзор: зачем выпущен, кому доступен, история поколений и разбор мифов вокруг релиза.
- [[amnezia-2-0/reference|AmneziaWG 2.0: справочник параметров]] — база, поверх которой надстроено третье поколение: мусорные пакеты, паддинг, диапазонные заголовки, язык CPS и все восемь его тегов.
- [[amnezia-3-0/client-5-0-0-5|AmneziaVPN 5.0.0.5]] — приложение, в котором третье поколение приехало пользователям.
- [[DPI/statistical-morphing-concept|Статистический морфинг трафика]] — общая идея правки длин и таймингов, частным случаем которой является content padding.
- [[protocols/00-overview|Карта протоколов обхода блокировок]] — место AmneziaWG среди остальных решений.
- 🔗 [amneziawg-go, тег v3.0.3](https://github.com/amnezia-vpn/amneziawg-go/tree/v3.0.3) — исходники, по которым сделан разбор.
---
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/amnezia-3-0/internals.md) · [весь репозиторий](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main).