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

53 KiB
Raw Permalink Blame History

date tags aliases link
2026-08-04
amneziawg
wireguard
обфускация
dpi
reverse-engineering
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
https://github.com/amnezia-vpn/amneziawg-go/tree/v3.0.3

🔬 AmneziaWG 3.0: внутреннее устройство протокола

[!info] О чём заметка Побайтовый разбор третьего поколения AmneziaWG по исходному коду: как собирается пакет, что именно шифрует защита заголовков, откуда берётся одноразовое число, как приёмная сторона опознаёт тип сообщения, не расшифровав его целиком, и какие следы протокол всё ещё оставляет в сети. Обзорная часть — назначение, доступность, история версий — в заметке amnezia-3-0/reference; параметры предыдущего поколения (Jc, S1S4, H1H4, язык CPS) подробно разобраны в amnezia-2-0/reference и здесь не повторяются.

TL;DR

  • Третье поколение добавляет к обфускации ровно три механизма: защита заголовков (ChaCha20 поверх готовых сообщений WireGuard), content padding (случайное удлинение полезной нагрузки вместо выравнивания по 16 байт) и диапазонные тайминги (новое случайное значение при каждом взводе таймера).
  • Одноразовое число (nonce) для шифра не передаётся отдельно — им служат первые 12 байт того самого случайного паддинга S1S4, который в 2.0 был просто мусором. Отсюда требование S1S4 ≥ 12 при включённой защите.
  • Приёмная сторона использует изящный приём: шифрует блок нулей, получая первые 4 байта потока ключа, и этим «хэшем типа» расшифровывает только поле типа — чтобы опознать сообщение до разбора остального пакета.
  • Тихое улучшение, не отмеченное нигде в документации: keepalive-пакеты теперь получают и паддинг S4, и content padding. В amnezia-2-0/reference они шли без 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 на теге 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. Описанную здесь механику 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 клиентская
S1S4 s1s4 int 1.0 (S3, S4 — в 2.0) серверная
H1H4 h1h4 диапазон uint32 1.0 (диапазоны — в 2.0) серверная
I1I5 i1i5 строка на языке 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: сначала уходят сигнатурные пакеты I1I5 (каждый — отдельная датаграмма), затем 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)   ← шифруется всё сообщение целиком

Отсюда единственное жёсткое ограничение третьего поколения: при непустом ключе все четыре значения S1S4 должны быть не меньше 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 первые четыре байта после паддинга были осмысленным числом из диапазона H1H4, а дальше шли постоянный индекс получателя и монотонно растущий счётчик.

Content padding: как считается добавка

Механизм устроен проще, чем можно подумать по названию, и умещается в один вспомогательный расчёт:

  • если ContentPaddingAddition не задан, возвращается признак «нет добавки», и работает обычное выравнивание длины до кратности 16;
  • если задан, из диапазона берётся случайное число;
  • добавка ограничивается сверху свободным местом до MTU (максимального размера передаваемого блока), чтобы не спровоцировать фрагментацию;
  • полученное количество нулевых байт дописывается в конец открытого текста, после чего всё вместе шифруется.

Два следствия, важных на практике. Первое: добавка попадает внутрь зашифрованной части, наблюдатель видит только изменившуюся длину пакета — сами байты паддинга он отличить от данных не может. Второе: заданный content padding отменяет штатное выравнивание по 16 байт. Именно в этом смысл механизма — предсказуемая сетка длин, кратных шестнадцати, сама по себе является признаком, по которому поток можно отнести к WireGuard-подобным.

[!note] Keepalive тоже паддится — это скрытое улучшение Служебные пакеты поддержания соединения проходят тот же путь, что и обычные: паддинг S4 им назначается при создании исходящего элемента, а content padding считается от нулевого размера полезной нагрузки. В amnezia-2-0/reference 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 модуля ядра: медиана интервала между рукопожатиями 15,0 с при S4 = 17 против 147148 с у контроля без S4 — примерно десятикратный рост. Перед каждым лишним рукопожатием, как обычно, уходят Jc мусорных и I1I5 сигнатурных пакетов, так что дефект не просто тратил трафик, а умножал самую заметную часть почерка протокола.

Границы дефекта по реализациям различаются. В модуле ядра он старше третьего поколения: issue #186 создано 15 июля 2026 года, за две недели до выхода 3.0 в модуле, — проверка is_keepalive = skb->len == message_data_len(0) в src/send.c выполнялась после добавления S4-мусора, то есть ошибка касалась и установок amnezia-2-0/reference с ненулевым S4. В go-движке дефект появился именно в линии 3.0 — на v0.2.19 та же проверка шла до добавления паддинга, и keepalive распознавался корректно (полевые замеры в том же issue: медиана 15,0 с на v3.0.3 против 132,0 с на v0.2.19).

Исправление: в модуле ядра — PR #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
Поле типа сообщения значение из диапазона H1H4, открыто зашифровано
Индекс получателя и счётчик открыты, предсказуемы зашифрованы
MAC1 и MAC2 в рукопожатиях открыты зашифрованы вместе со всем сообщением
Паддинг S1S4 чистый мусор мусор плюс источник одноразового числа
Минимум S1S4 0 (любое) 12 при включённой защите заголовков
Момент добавления S4 сдвиг готового пакета вправо место резервируется заранее
Keepalive без S4, ровно 32 байта с S4 и content padding
Длина полезной нагрузки выравнивание до 16 байт случайная добавка из диапазона
Тайминги константы WireGuard диапазоны, новое значение на каждый взвод
Мусорные и сигнатурные пакеты Jc/Jmin/Jmax, I1I5, язык 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, согласуется с устройством обеих реализаций: и 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 пусто; отсюда жалобы вида «интерфейс просто не поднимается, в логе ничего». Правило для рецептов I1I5, которые предлагаются другим людям: использовать только пересечение {b, r, rc, rd, t}. Теги <d>/<ds>/<dz> в пакетах I1I5 и так почти бесполезны — они получают на вход пустые данные; полный разбор go-набора — в amnezia-2-0/reference. В линиях 3.0 и 3.1 наборы обеих реализаций идентичны, go-набор не менялся с 2.0.

Практика: как это отлаживать

  • Сгенерировать ключ защиты заголовков: awg genkey из пакета amneziawg-tools версии v3.0.20260730 или новее — более старые утилиты не знают новых ключей конфигурации и молча их не передадут.
  • Проверить, что все четыре значения S1S4 не меньше 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) — собирать I1I5 только из тегов {b, r, rc, rd, t}: наборы тегов реализаций расходятся, подробности в «Ловушках» выше.
  • Не копировать значения I1I5 из примеров: повторяющаяся у тысяч установок сигнатура работает против вас, чему посвящён отдельный разбор в amnezia-3-0/reference.

📚 См. также

  • amnezia-3-0/reference — обзор: зачем выпущен, кому доступен, история поколений и разбор мифов вокруг релиза.
  • amnezia-2-0/reference — база, поверх которой надстроено третье поколение: мусорные пакеты, паддинг, диапазонные заголовки, язык CPS и все восемь его тегов.
  • amnezia-3-0/client-5-0-0-5 — приложение, в котором третье поколение приехало пользователям.
  • DPI/statistical-morphing-concept — общая идея правки длин и таймингов, частным случаем которой является content padding.
  • protocols/00-overview — место AmneziaWG среди остальных решений.
  • 🔗 amneziawg-go, тег v3.0.3 — исходники, по которым сделан разбор.

[!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.