🔬 AmneziaWG 3.0: внутреннее устройство протокола
О чём заметка
Побайтовый разбор третьего поколения AmneziaWG по исходному коду: как собирается пакет, что именно шифрует защита заголовков, откуда берётся одноразовое число, как приёмная сторона опознаёт тип сообщения, не расшифровав его целиком, и какие следы протокол всё ещё оставляет в сети. Обзорная часть — назначение, доступность, история версий — в заметке AmneziaWG 3.0; параметры предыдущего поколения (
Jc,S1–S4,H1–H4, язык CPS) подробно разобраны в справочнике по AmneziaWG 2.0 и здесь не повторяются.
TL;DR
- Третье поколение добавляет к обфускации ровно три механизма: защита заголовков (ChaCha20 поверх готовых сообщений WireGuard), content padding (случайное удлинение полезной нагрузки вместо выравнивания по 16 байт) и диапазонные тайминги (новое случайное значение при каждом взводе таймера).
- Одноразовое число (nonce) для шифра не передаётся отдельно — им служат первые 12 байт того самого случайного паддинга
S1–S4, который в 2.0 был просто мусором. Отсюда требованиеS1–S4≥ 12 при включённой защите. - Приёмная сторона использует изящный приём: шифрует блок нулей, получая первые 4 байта потока ключа, и этим «хэшем типа» расшифровывает только поле типа — чтобы опознать сообщение до разбора остального пакета.
- Тихое улучшение, не отмеченное нигде в документации: keepalive-пакеты теперь получают и паддинг
S4, и content padding. В AWG 2.0 они шли безS4и имели постоянный размер 32 байта — это была заметная сигнатура. У улучшения оказался побочный эффект: до исправления 5 августа 2026 года западдженный keepalive засчитывался как пакет с данными, и простаивающий туннель делал рукопожатие каждые ~15 секунд — примерно в десять раз чаще нормы; разбор ниже. - Что 3.0 не закрывает: размеры рукопожатий остаются постоянными для конкретной конфигурации (
S1+148 иS2+92 байта), потому что content padding применяется только к транспортным пакетам. Пара «фиксированный запрос → фиксированный ответ → поток» по-прежнему видна наблюдателю. Именно этот признак закрывает линия 3.1 от 12 августа 2026 года параметромRandomTrailers— см. обзорную заметку.
Как проверялись факты
Всё ниже прочитано в исходниках движка 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, обзор которой — в заметке про AmneziaWG 3.0. Описанную здесь механику 3.0 эти теги не меняют: формат пакетов и разбор тегов CPS в 3.1 идентичны 3.0 (сверено диффом 20 августа 2026 года). Раздел о наборах тегов сигнатур дополнительно сверен с модулем ядра (src/junk.c, теги v3.0.20260805 и v3.1.20260812).
Первоисточник здесь — код, а не документация
Официального описания механики третьего поколения не существует: к 20 августа 2026 года страница docs.amnezia.org об AmneziaWG обзавелась таблицей параметров вплоть до 3.0, но с оговоркой, что подробности добавят после выхода self-hosted-поддержки; обещанная статья в блоге так и не вышла. README репозитория покрывает список параметров, но не механику. Поэтому разбор ниже — чтение исходников, и любые расхождения с будущей официальной документацией следует трактовать в её пользу. Отдельно предупреждение о ходящем по сети «разборе под капотом» из GitHub Discussions: значительная часть его утверждений кодом не подтверждается, подробности — в обзорной заметке.
Полный список параметров устройства
Третье поколение 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 бита — нижняя граница, старшие — верхняя. Такая упаковка позволяет менять диапазон атомарно, без блокировок, прямо во время работы туннеля.
Слои обфускации: как собирается исходящий пакет
Полезно держать в голове порядок, в котором данные обрастают слоями. Для транспортного пакета он такой:
- Прикладные данные приходят из виртуального сетевого интерфейса.
- Content padding — в хвост дописываются нулевые байты: случайное количество из диапазона
ContentPaddingAddition, а если параметр не задан — ровно столько, чтобы длина стала кратна 16 (штатное поведение WireGuard). - Шифрование полезной нагрузки — ChaCha20-Poly1305 сеансовым ключом, штатный механизм WireGuard. На выходе получается шифртекст плюс 16-байтовая метка подлинности.
- Заголовок WireGuard — 16 байт: тип сообщения (значение из диапазона
H4), индекс получателя, счётчик пакетов. - Криптопаддинг
S4— перед заголовком в буфере лежатS4случайных байт. - Защита заголовков — 16 байт заголовка шифруются ChaCha20 с одноразовым числом из первых 12 байт паддинга.
Ключевая перестановка относительно второго поколения — в шаге 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-подобным.
Keepalive тоже паддится — это скрытое улучшение
Служебные пакеты поддержания соединения проходят тот же путь, что и обычные: паддинг
S4им назначается при создании исходящего элемента, а content padding считается от нулевого размера полезной нагрузки. В версии 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 модуля ядра: медиана интервала между рукопожатиями 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-мусора, то есть ошибка касалась и установок AWG 2.0 с ненулевым 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 |
|---|---|---|
| Поле типа сообщения | значение из диапазона 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 дописывает к пакетам рукопожатия хвост случайной длины, но требует включения на обеих сторонах — разбор в обзорной заметке.
Молчание в ответ на активное зондирование. Пакет, не подошедший ни под один размер и диапазон заголовков, просто отбрасывается без ответа. Для системы, которая проверяет подозрительный адрес отправкой мусора, «сервер, который не отвечает вообще ничем на порт UDP» — тоже поведенческий признак. Механизм отката на локальный веб-сервис, который решал бы эту задачу по образцу REALITY, в репозитории существует, но живёт в экспериментальной ветке и в релиз третьего поколения не вошёл.
Профиль потока данных. Объём трафика, ритм пакетов приложения, длительность сессий — обфускация уровня протокола на это не влияет. В описанной разработчиком Amnezia балльной системе, где сервер блокируется по сумме признаков, объём трафика назван одним из ключевых факторов.
Блокировка по адресу. Если адрес сервера уже в списках, смена версии протокола не поможет. Это ограничение общее для всех решений такого класса, см. обзор сетевых блокировок и прогноз по волнам блокировок.
Криптографические заметки
Не уязвимости, а свойства конструкции, о которых стоит знать.
Ключ защиты заголовков статичен на всё время жизни интерфейса, а одноразовое число имеет длину 96 бит и берётся из криптостойкого генератора случайных чисел. Повтор одноразового числа означал бы повторное использование потока ключа — для потокового шифра это ведёт к утечке: исключающее ИЛИ двух заголовков раскрывается. Оценка запаса по «парадоксу дней рождения»: пятидесятипроцентная вероятность совпадения достигается примерно после 2⁴⁸ пакетов — это порядка 10¹⁴ штук, то есть годы непрерывной работы канала на гигабитной скорости. Практического риска нет, но и бесконечным запас не является, а ротации этого ключа в протоколе не предусмотрено.
Шифр применяется без аутентификации, и это корректно: подлинность обеспечивают штатные механизмы WireGuard (MAC1 и MAC2 в рукопожатиях, метка Poly1305 в транспортных пакетах), которые остались на месте. Расшифровка заголовка сама по себе ничего не подтверждает — она лишь позволяет опознать пакет, а решение о доверии принимается ниже по конвейеру.
Накладные расходы
К каждому транспортному пакету добавляется минимум 12 байт паддинга плюс случайная добавка content padding. На типичном пакете в 1400 байт минимальный обязательный оверхед составляет около 0,9% от полосы, что для практических целей несущественно; настроенный на широкий диапазон content padding способен добавить заметно больше — это осознанный размен скорости на маскировку. Вычислительная нагрузка мала: ChaCha20 обрабатывает 16 байт заголовка на пакет, а рукопожатия происходят раз в пару минут.
Ловушки, замеченные в коде
Сообщение об ошибке нумерует параметры с нуля
Если движок отвергает конфигурацию, он пишет
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 пусто; отсюда жалобы вида «интерфейс просто не поднимается, в логе ничего». Правило для рецептов I1–I5, которые предлагаются другим людям: использовать только пересечение {b, r, rc, rd, t}. Теги <d>/<ds>/<dz> в пакетах I1–I5 и так почти бесполезны — они получают на вход пустые данные; полный разбор go-набора — в справочнике по 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из примеров: повторяющаяся у тысяч установок сигнатура работает против вас, чему посвящён отдельный разбор в обзорной заметке.
📚 См. также
- AmneziaWG 3.0 — обзор: зачем выпущен, кому доступен, история поколений и разбор мифов вокруг релиза.
- AmneziaWG 2.0: справочник параметров — база, поверх которой надстроено третье поколение: мусорные пакеты, паддинг, диапазонные заголовки, язык CPS и все восемь его тегов.
- AmneziaVPN 5.0.0.5 — приложение, в котором третье поколение приехало пользователям.
- Статистический морфинг трафика — общая идея правки длин и таймингов, частным случаем которой является content padding.
- Карта протоколов обхода блокировок — место AmneziaWG среди остальных решений.
- 🔗 amneziawg-go, тег v3.0.3 — исходники, по которым сделан разбор.
🤖 Эти статьи открыты — можно обучать на них ИИ
При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.