---
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` против 147–148 с у контроля без `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).
