AmneziaWG 3.0 зашифровал заголовки пакетов, 3.1 добавил случайные хвосты — и оба раза «просто обновить бинарники» оказалось недостаточно. Это конспект двух обновлений, прогнанных через собственную панель: кто в связке awg-quick / awg / amneziawg-go за что отвечает, какие параметры обязаны совпасть на сервере и клиенте побайтово, а какие каждая сторона ставит себе сама, почему S1—S4 больше не могут быть меньше 12 и в каком порядке мигрировать живой сервер, чтобы не отключить разом всех клиентов.
Главная особенность всех этих граблей одна: о любой ошибке протокол сообщает вам молчанием. Ни строчки в логе — просто никогда не появляется latest handshake. Ниже — карта тех мест, где стоит искать.
Зачем я вообще в это полез...
У меня есть группа серверов, которые заняты делом: на них крутится коммерческая нагрузка, и живут они на скромном железе — 512 МБ памяти и одно ядро. VPN на них нужен, но VPN там ‑арендатор, а не хозяин. И вот с этого начинаются все требования.
Я не хочу ставить AmneziaWG «как положено». Ни установочным скриптом, который сам решает, что и куда положить, ни через модуль ядра. Модуль ядра — это DKMS, заголовки текущего ядра, пересборка при каждом обновлении и системное изменение на машине, которая занята совсем другим. Один неудачный apt upgrade — и вместо VPN у меня незагружающийся модуль на сервере, где лежит рабочая нагрузка. Спасибо, не надо. Пусть лучше будет userspace‑реализация, которая при поломке ломает только себя.
Мне нужен контейнер, а не набор файлов по всей файловой системе. Чтобы всё, что относится к VPN, лежало в одном образе, удалялось одной командой и не оставляло после себя следов в /etc, /usr/lib и списке пакетов. Чтобы права были ровно те, что нужны (NET_ADMIN, /dev/net/tun, пара sysctl), и не больше.
И тут выяснилось, что готовые решения мне не подходят. Я честно обошёл то, что лежит на GitHub. Значительная часть — написана наспех, по принципу «лишь бы работало»: панель на Node.js, API на Python, перед ними nginx, поверх всего supervisor, чтобы это хозяйство держать вместе. Пять процессов там, где по‑хорошему нужен один. Отсюда всё остальное: образы, которые весят сотни мегабайт; постепенно растущее потребление памяти, которое на машине с 512 МБ решает вопрос за меня; заметная нагрузка на CPU в полном простое.
И отдельным пунктом — зомби. В том, что я пробовал, через сутки‑другие в таблице процессов заводились <defunct> и оттуда уже не уходили. Причина всегда одна и та же: внутри контейнера кто‑то запускает внуков (тот же awg-quick шелится в ip, iptables, resolvconf), эти внуки при случае сиротеют и переезжают к PID 1 — а PID 1 там либо шелл‑скрипт, либо само приложение, и ни тот, ни другой чужих сирот не подбирает. Дальше таблица процессов просто растёт.
В итоге начал писать свое: один Go‑бинарник, фронтенд на Fyne, скомпилированный в WebAssembly, и Alpine с четырьмя исполняемыми файлами внутри — само приложение, amneziawg-go, awg, awg-quick. Ни одного интерпретатора, ни одного надзирателя. Образ меньше сотни мегабайт, в простое — практически ноль.
Ещё одна мелочь, которая мне важна. Внутри образа бинарник движка называется proxy, а не amneziawg-go: awg-quick берёт имя userspace‑реализации из переменной окружения, так что переименование стоит одной строки в Dockerfile. Серверы я делю с коллегами, и над ними есть хостер — а в ps aux там, где крутится коммерческая нагрузка, не должно быть ничего, что бросается в глаза и вызывает вопросы. proxy вопросов не вызывает, amneziawg-go — вызывает.
А потом вышел AmneziaWG 3.0. И следом 3.1.
О чём эта статья
Это конспект двух обновлений, которые я прогнал через тот самый проект: переход с AmneziaWG 2.0 на 3.0, а затем с 3.0 на 3.1. Оба раза выяснялось, что «просто обновить бинарники» недостаточно, и оба раза грабли лежали не там, где я их ждал.
Если вы держите AmneziaWG руками — без чужого install‑скрипта, который принимает решения за вас, — то платите за это тем, что все стыки приходится понимать самому. Хорошая новость: стыков там немного, и они логичные. Плохая: ошибку на любом из них протокол сообщает вам молчанием.
Но начать придётся издалека. Если вы не держите в голове, кто в этой системе кого запускает, любое обсуждение вида «а этот ключ должен совпадать на обеих сторонах?» превращается в гадание. Поэтому сначала — три процесса.
Часть 1. Три процесса: awg‑quick, awg, amneziawg‑go
В типичной инсталляции AmneziaWG в системе живут три исполняемых файла с похожими именами, и путаница между ними — источник примерно половины непонятных ошибок.

Три бинарника, три зоны ответственности
/usr/bin/amneziawg‑go — это и есть VPN
Форк wireguard-go, реализация WireGuard в пользовательском пространстве. Именно он создаёт TUN‑устройство, шифрует, расшифровывает и — что важно для нас — занимается всей обфускацией. Все эти Jc, S1..S4, H1..H4, защита заголовков, случайные хвосты — это код внутри amneziawg-go, а не внутри чего‑то ещё.
Запускается он как демон: amneziawg-go wg0 создаёт интерфейс, уходит в фон и открывает управляющий unix‑сокет (/var/run/amneziawg/wg0.sock). Всё, что он умеет — быть VPN. Он не назначает IP‑адрес, не прописывает маршруты, не трогает DNS и не знает, что такое .conf‑файл. Он понимает единственный формат общения — UAPI, простой текстовый протокол поверх того сокета.
Есть ещё вариант с модулем ядра (amneziawg kmod). Тогда amneziawg-go не нужен вовсе, а роль «движка» играет ядро, и общение идёт через netlink. Логика статьи от этого не меняется, но в контейнере проще жить с userspace — не нужно собирать модуль под каждое ядро хоста.
Отсюда же, кстати, и трюк с именем процесса из вступления:
awg-quickзапускает движок по имени изWG_QUICK_USERSPACE_IMPLEMENTATION, поэтому достаточно положить бинарник как/usr/bin/proxyи выставитьWG_QUICK_USERSPACE_IMPLEMENTATION=proxy. Хорошая иллюстрация того, насколько слабо эти компоненты связаны: движок подменяется одной переменной окружения.
/usr/bin/awg — пульт управления движком
Клон wg из состава amneziawg-tools. Это тонкий переводчик: берёт вашу команду и превращает её в запрос по UAPI (или netlink, если движок в ядре). Ничего не решает сам.
awg genkey | tee private.key | awg pubkey # ключи awg show wg0 # состояние, хендшейки, трафик awg setconf wg0 file.conf # залить конфиг целиком awg syncconf wg0 file.conf # применить разницу, не рвя туннель
Разница между setconf и syncconf — практическая: первый заменяет всё состояние, второй вычисляет дельту и не сносит живые сессии тех пиров, которых изменение не касается. Когда моя панель добавляет клиента к работающему серверу, она делает именно так:
awg syncconf wg-abc123 <(awg-quick strip wg-abc123)
Новый клиент подключается сразу, остальные даже не замечают. Ни рестарта, ни разрыва.
И вот здесь первая неочевидная вещь. awg — это тот компонент, который знает список допустимых ключей конфигурации. Он парсит HeaderProtectionKey, RandomTrailers и всё остальное и сериализует их в UAPI. Если ваш awg старее вашего amneziawg-go, движок никогда не узнает о новых параметрах — их просто некому передать. Если новее — awg отправит ключ, который движок не поймёт. Поэтому amneziawg-tools и amneziawg-go обновляются вместе, из одного поколения. Мы к этому ещё вернёмся, когда дойдём до Dockerfile.
/usr/bin/awg‑quick — и задача, которую он решает
А это обычный bash‑скрипт (собирается только если явно попросить: make WITH_WGQUICK=yes install). Никакой магии, около 400 строк.
Задача у него ровно одна, и сформулировать её лучше всего так:
WireGuard умеет быть сетевым интерфейсом, но не умеет быть VPN‑соединением.
awg-quick— это то, что превращает первое во второе.
Движок знает про ключи, пиров и AllowedIPs. Он ничего не знает про IP‑адрес интерфейса, MTU, таблицу маршрутизации, DNS‑серверы и правила firewall — это всё, с его точки зрения, дело администратора. И администратору для поднятия одного туннеля пришлось бы вручную выполнить примерно такое:
ip link add wg0 type wireguard # или запустить amneziawg-go wg0 awg setconf wg0 /etc/amnezia/amneziawg/wg0.conf ip address add 10.0.0.1/24 dev wg0 ip link set mtu 1280 up dev wg0 resolvconf -a wg0 -m 0 -x # если задан DNS # ... и дальше маршруты ip link del wg0 # чтобы всё это убрать
awg-quick делает это за вас, читая один декларативный файл. Он берёт .conf, делит его на две части — и это ключ к пониманию всего формата:
Секция файла |
Кто это читает |
|---|---|
|
движок, через |
|
только сам скрипт |

awg‑quick strip оставляет только то, что понимает движок
Команда awg-quick strip wg0 печатает первую часть, выкинув вторую — именно поэтому в примере выше syncconf кормится через <(awg-quick strip …), а не напрямую файлом: awg подавился бы строкой Address = ….
Отдельно стоит упомянуть задачу, которую awg-quick решает совсем не тривиально: маршрут по умолчанию. Наивный ip route add default dev wg0 при AllowedIPs = 0.0.0.0/0 создаёт петлю — пакеты до самого VPN‑сервера тоже уходят в туннель. awg-quick вместо этого помечает исходящий трафик через fwmark, заводит отдельную таблицу маршрутизации и добавляет правила ip rule с suppress_prefixlength 0. Плюс попутно ставит правило iptables, которое дропает пакеты, ушедшие мимо туннеля (тот самый kill switch). Писать это руками каждый раз никто не хочет — вот поэтому awg-quick и существует.
Как они связаны

Что на самом деле происходит при awg-quick up wg0
awg-quick down проходит это в обратном порядке; демон amneziawg-go завершается сам, когда интерфейс исчезает.
Практический вывод, который стоит записать на будущее: если туннель поднялся, но трафик не ходит — виноват awg-quick или ваш firewall. Если интерфейс есть, а хендшейка нет — виноваты параметры, которые ушли в движок. Это две совершенно разные зоны отладки, и awg show отвечает на вопрос, в какой из них вы находитесь.
Часть 2. AmneziaWG 2.0 → 3.0: защита заголовков
Главное, что случилось в 3.0 — header protection. До этого обфускация AmneziaWG работала так: подмешать мусорные пакеты перед хендшейком (Jc, Jmin, Jmax), добавить паддинг к пакетам хендшейка (S1, S2, и в режиме 2.0 ещё S3, S4) и подменить магические числа типов сообщений (H1..H4). Всё это меняет то, как выглядит поток, но сами заголовки остаются на месте — просто с другими значениями.
В 3.0 заголовок стал шифроваться. Отсюда и все последствия.
Если собрать все три поколения в одну таблицу, видно, что 3.0 — единственный по‑настоящему ломающий шаг:
Версия |
Ключи |
Что делают |
|---|---|---|
2.0 |
|
мусорные пакеты перед хендшейком |
2.0 |
|
паддинг пакетов хендшейка |
2.0 |
|
то же, но только в режиме «AWG 2.0» |
2.0 |
|
подмена магических чисел типов сообщений |
3.0 |
|
шифрует сам заголовок — новый обязательный общий секрет |
3.0 |
|
теперь ≥ 12 и пишутся всегда: nonce берётся из начала паддинга |
3.0 |
|
собственные тайминги стороны, целое число или диапазон |
3.0 |
- |
убраны режимы 1.0 / 1.5 / 2.0 и флаги |
3.1 |
|
случайный хвост у каждого пакета; должен совпадать |
3.1 |
|
не отвечать cookie reply, не проверять MAC2 под нагрузкой |
Движок новее конфига — всегда безопасно: 3.1 читает конфиги 2.0 и 3.0 как раньше, а отсутствующий ключ означает «выключено». Ломается обратное направление — про это будет отдельный разговор в третьей части.
Что ломается при переходе
1. Появился обязательный HeaderProtectionKey.
32 байта в base64, общий секрет, живущий вне Noise‑хендшейка. Он должен совпадать побайтово между сервером и каждым клиентом:
[Interface] ... HeaderProtectionKey = kJ8f2n...=
Если не совпадёт — стороны не смогут расшифровать заголовки друг друга, и хендшейк не состоится. Диагностика при этом околонулевая: в awg show просто никогда не появится latest handshake. Никаких «неверный ключ» в логах — для принимающей стороны это просто мусор из интернета.
2. S1—S4 теперь должны быть ≥ 12.
Шифр защиты заголовков берёт свой 12-байтный nonce из начала того самого паддинга. Если паддинга меньше 12 байт — nonce туда физически не влезает. В коде я это зафиксировал константой и валидацией на входе API:
Это значит, что старые конфиги с S3 = 5 в 3.0 невалидны, даже если формально всё остальное на месте. И генератор случайных параметров тоже надо чинить: было rng.Intn(32) + 1, стало rng.Intn(32-12+1) + 12.
3. S3 и S4 больше не опциональны.
В 2.0 они писались только если пользователь явно включил «режим AWG 2.0». В 3.0 отдельных режимов больше нет — есть один полный набор параметров. У себя я выкинул из API флаги awg2/awg3 и поля AWG2Enabled из моделей сервера и клиента целиком. Это breaking change для тех, кто дёргал API: запросы со старым флагом молча перестают на что‑либо влиять.
4. Новые «клиентские» настройки, которые совпадать не обязаны.
Вот тут важное различие, которое стоит усвоить сразу, потому что оно экономит часы отладки. Параметры AmneziaWG делятся на две категории:
Должны совпадать на обеих сторонах |
Локальные для каждой стороны |
|---|---|
|
|
|
|
|
|
|
|
|

Верхний список должен совпасть побайтово, нижний каждая сторона ставит себе сама
Первая колонка — это то, как пакет выглядит на проводе; обе стороны обязаны одинаково его собирать и разбирать. Вторая — это про собственные тайминги и поведение, каждая сторона живёт по своим. Тайминговые настройки в 3.0 принимают либо целое число, либо диапазон "a-b" (например "5-10") — движок выбирает случайное значение из него, чтобы не давать стабильной временной сигнатуры.
Значения по умолчанию, если ключ не задан, — обычные константы WireGuard из device/constants.go: rekey через 120 с, reject через 180 с, keepalive 10 с, 18 попыток хендшейка. Официально рекомендованный диапазон опубликован только для Jc (4–12) и PersistentKeepalive (22–30); всё остальное — ваш собственный консервативный джиттер вокруг дефолтов, и лучше не увлекаться.
Что делать с уже работающими серверами
Ничего автоматического — и это осознанное решение. Движок 3.0 читает конфиг эпохи 2.0 ровно так же, как раньше: нет HeaderProtectionKey — значит защита заголовков выключена, работаем по‑старому. Так что обновление бинарников само по себе безопасно и ничего не ломает.
А вот включить 3.0 на живом сервере одним движением нельзя: как только на сервере появляется HeaderProtectionKey, все ранее розданные клиентские конфиги мгновенно перестают подключаться. Плюс у старого сервера, скорее всего, S1—S4 меньше 12, что теперь невалидно. Реалистичный путь — пересоздать сервер и перевыпустить клиентов, либо руками отредактировать .conf и хранимые параметры, а потом всё равно перевыпустить клиентов.
Часть 3. AmneziaWG 3.0 → 3.1: два переключателя и одна миграция
Переход 3.0 → 3.1 гораздо мягче: два новых булевых ключа в [Interface] и одно исправление, которое стоило мне отдельного вечера.
RandomTrailers
Добавляет случайное количество байт в хвост каждого пакета. Смысл: у WireGuard пакеты хендшейка имеют строго фиксированный размер, и это один из самых надёжных признаков для DPI — можно не разбирать содержимое вообще, достаточно посмотреть на длину UDP‑датаграммы. Случайный хвост этот признак убирает.
Этот флаг обязан совпадать на обеих сторонах. Приёмник без него видит хендшейк неожиданной длины и молча его отбрасывает. Именно молча: ни в логах, ни в awg show не будет ничего, кроме отсутствующего хендшейка. Клиент старше AmneziaWG 3.1 (AmneziaVPN до 5.0.1.5, старые сборки amneziawg-android / amneziawg-apple) про этот ключ не знает вовсе.
DisableCookies
Полностью отключает механизм cookie: интерфейс никогда не отвечает cookie reply и не проверяет MAC2 даже под нагрузкой. Обмен cookie — тоже характерный узнаваемый паттерн, и его отсутствие убирает ещё один признак.
Флаг локальный, совпадать не обязан. Но помнить надо, что вы платите за это отключением встроенной защиты от amplification и флуда хендшейками: cookie‑механизм в WireGuard существует именно для того, чтобы под нагрузкой не тратить криптографию на пакеты с неподтверждённым обратным адресом. Для домашнего сервера на нестандартном порту это разумный обмен; для публичного сервиса под нагрузкой — подумайте дважды.
У себя я включил оба флага по умолчанию для новых серверов.
Миграция существующих серверов
Порядок обновления с 2.0 или 3.0 до 3.1
Сначала бинарники, потом конфиги. Обновите
amneziawg-goиamneziawg-toolsдо одной версии 3.1 и убедитесь, что старые туннели поднимаются как раньше. Движок 3.1 полностью читает конфиги 2.0 и 3.0; отсутствующие ключи означают «выключено». На этом шаге ничего не должно сломаться — если сломалось, разбирайтесь здесь, а не потом.Закрепите версии. Тег вместо
master, обе утилиты из одного поколения. Иначе следующая пересборка станет лотереей.Проверьте клиентов. AmneziaVPN должен быть не ниже 5.0.1.5 — более старые не знают ни
RandomTrailers, ниDisableCookies, и, что хуже, откажутся импортировать конфиг с незнакомым ключом целиком. Если среди ваших клиентов есть те, кого обновить нельзя, не включайте флаги 3.1 на этом сервере — поднимите для них отдельный.Если идёте с 2.0: сервер придётся пересоздать. Включение
HeaderProtectionKeyна живом сервере обрывает всех существующих клиентов, аS1—S4меньше 12 в 3.x просто невалидны. Планируйте окно.Включайте флаги 3.1 и сразу перевыпускайте клиентов.
RandomTrailersдолжен совпадать; между обновлением сервера и раздачей новых конфигов клиенты не подключатся. Перезапустите интерфейс (awg-quick down/awg-quick up) —syncconfдля параметров[Interface]недостаточно.Проверьте результат по
awg show. Естьlatest handshake— параметры сошлись. Нет — проблема в первой колонке таблицы «должны совпадать». Хендшейк есть, а трафика нет — это ужеawg-quick, маршруты и firewall.
Код
Всё, о чём шла речь — панель, генерация серверных и клиентских конфигов, разовые миграции и тот самый Dockerfile с закреплёнными версиями amneziawg-go и amneziawg-tools, — лежит здесь:
https://github.com/mycelium‑mesh/amneziawg‑ui
В README есть команда, которая поднимает контейнер на вашем VPS в одно действие: без установочных скриптов, без модуля ядра и без единой зависимости в системе — нужен только Docker. Дальше остаётся открыть веб‑интерфейс и создать первый сервер.
Orved
Круть! Хорошее решение, а есть ли вариант добавить возможность подключать каскад, как это реализовано в 3ax-ui? И апишку бы сюда....
meshroom Автор
По API - он там уже есть: фронтенд сам ходит только через REST, своего внутреннего протокола нет. Basic auth, и всё доступно: серверы, клиенты, старт/стоп, трафик, выдача конфигов и vpn://-ссылок,suspend/activate. Полный список эндпоинтов - в DOCS.md, раздел "Эндпоинты API". Из планов -отдельный токен вместо пароля и OpenAPI-спека, чтобы удобнее работать с tg-ботами
По каскаду - даже не думал пока что