TLDR: Для Mikrotik'ов на базе Arm, Arm64 и Amd64 создана рабочая реализация AmneziaWG для подключения к AmneziaWG и AmneziaVPN серверам. Для воспроизводимой настройки создан небольшой оффлайн(!) конфигуратор, который по входному amneziawg.conf формирует набор команд для RouterOS Terminal (и скрипт очистки): версия на C (летает, выдает на 30% больше bandwidth чем версия Go), версия на GO (тоже быстрая, но в целом лучше оттестирована), запасной Go на gitlab. Итоговый Go-контейнер весит очень мало, почти не потребляет ЦПУ (1-2%), использует 7-10 МБ Ram на ARM64.

Github: C, GO (27.02.2026 оригинальный первый репо попал под бан гитхаба. Второй акк с копией тоже забанили. Кажется, гитхабу не понравились сетевые тесты. Постараюсь не допускать таких ошибок. На всякий: копия в гитлаб)

upd: Добавлена поддержка протокола AmneziaWG v2.

upd2: провели тестовые замеры скорости. На линке 300мбит выжали +-250мбит (теоретический предел - 280 мбит).

upd3: в комментариях несколько человек делятся результатами. Пока-что это лучшее решения для Mikrotik. Призываю и вас делиться замерами производительности <3

upd4: Переписали на C. Предварительно, он в 3 раза меньше потребляет ЦПУ, и на 30% больше прокачивает чем реализация на Go.

На 300мбит-канале выжимаем 200-270мбит. За многочисленные испытания спасибо @wiktorbgu!

Disclaimer.  Материал носит образовательный и ознакомительный характер и посвящён вопросам совместимости реализаций WireGuard/AmneziaWG и разбору сетевой упаковки пакетов/handshake. Примеры приводятся для сценариев защищённого удалённого доступа к собственным системам и инфраструктуре (администрирование, корпоративные сети, тестовые стенды). Не используйте информацию из статьи для нарушения законодательства РФ и иных применимых норм, а также правил площадок и провайдеров. Автор не несёт ответственности за последствия использования описанных подходов.

Одним очередным томным вечером, приходя домой вдруг снова обнаруживаешь, что соединение с сервером нестабильно и периодически обрывается. Вполне безобидный сервер, для доступа к рабочим инструментам вдруг стал недоступен. В очередной раз решил поискать в интернете, может кто-то уже реализовал нормальный клиент AmneziaWG в Mikrotik:

На форуме mikrotik так ничего и не появилось, а из самых доступных реализаций находится только этот контейнер на 36 МБ (от @wiktorbgu, он много лет поддерживал его, поставьте + в карму за поддержку) с полной реализацией AmneziaWG-клиента, но на моем стареньком Mikrotik доступно всего 7 МБ :'( , поэтому этот вариант отпадает. (UPD: по пользовательским замерам, у моего решения нагрузка на CPU примерно в 1,5-2 раза ниже, чем у контейнера на 36 МБ по ссылке выше).

Выхода нет. Почему бы не написать реализацию самому с нуля?

Варианты реализации

Первым делом полез искать вариант реализации своего Package, который можно было бы установить максимально нативно, чтобы он работал на уровне ядра Mikrotik. К сожалению, этот вариант отпадает первым - Routerboard закрытая проприетарная система, а пакеты требуют цифровую подпись.

Второй вариант - создать свой минималистичный container по реализации AmneziaWG клиента. Туда нужно прокинуть все настройки подключения, научить его не только подключаться и работать с криптографией, но и корректно маршрутизировать сквозь себя трафик. При реализации очень легко ошибиться с криптографией, а сам бинарник, по моим подсчетам, будет в районе ~15 МБ. Не говоря уже о дублировании почти всего, что в mikrotik уже есть.

Третий вариант пришел неожиданно. У нас есть Mikrotik, в котором из коробки уже есть прекрасная реализация Wireguard. А AmneziaWG это тот же Wireguard, с небольшими отличиями. Вся разница - во фрейминге пакетов: перед тем как безопасное соединение установится, две точки должны обменяться рандомными мусорными пакетами. А дальше идет обычное соединение Wireguard!

То есть всё, что нам нужно - это реализовать первую фазу установления соединения: "поздороваться" с сервером на языке AmneziaWG, а дальше просто передать всё управление интерфейсу Wireguard'а! Всего лишь обменяемся мусорными пакетами (так думал я поначалу, и глубоко ошибался), а всю дальнейшую маршрутизацию, криптографию и пиринг передадим в руки нативного Wireguard в mikrotik. Такая реализация простой udp-прокси по моим подсчетам могла уложиться в виде go-бинарника размером 3-4 МБ, что соответствует моим ожиданиям.

В итоге мы по-максимуму переиспользуем уже готовый и реализованный Wireguard в самом Mikrotik, а допишем и реализуем только то, что ему не хватает - Handshake с AmneziaWG!

Mikrotik Wireguard -> UDP -> [awg-proxy container] -> UDP -> AmneziaWG Server

Приступаем к реализации

Был выбран язык Go, который будет компилироваться из scratch-контейнера в самый минималистичный вариант. Go довольно быстрый, позволяет легко собрать бинарник, не требующий скрытых зависимостей, да и вообще я его сейчас практикую, почему бы и нет?

Вполне возможно, реализация на С была бы ещё меньшего размера, но у меня в нём мало практики, а я не собирался стрелять себе в ноги, я лишь хочу решить свою маленькую проблему. Так что выбор был остановлен на Go.

Протокол WireGuard изнутри

Чтобы понять, что именно трансформирует прокси, нужно заглянуть в формат пакетов WireGuard. Не в криптографию (она нас не касается), а именно в структуру датаграмм:

WireGuard использует ровно четыре типа UDP-сообщений. Тип кодируется как uint32 в little-endian порядке в первых 4 байтах каждого пакета:

Тип

Значение

Размер

Назначение

1

Handshake Init

148 байт (фикс.)

Инициация handshake, первое сообщение Noise IK

2

Handshake Response

92 байта (фикс.)

Ответ на handshake, второе сообщение Noise IK

3

Cookie Reply

64 байта (фикс.)

Cookie для защиты от DoS (rate limiting)

4

Transport Data

32+ байт (перем.)

Зашифрованные данные пользователя

UPD: здесь было куча шелухи, которая никому было не интересна. И после неё я ещё 3 раза переписал реализацию, так-что прошлый скучный материал потерял актуальность, а про новые способы я скорее всего напишу отдельную статью. Поэтому оставлю только самое важное.

Ловушка с MAC1: 3 дня в тишине

Баг, над которым просидел 3 дня.

Вроде работает но не работает

Я написал прокси, проверил код: пакет превращается из WG в AWG и обратно без потерь: тип восстанавливается, payload побайтово совпадает с оригиналом. Junk-пакеты генерируются правильного размера. Паддинг добавляется и снимается.

Запускаю прокси локально, указываю на реальный AWG-сервер. Конфигурация - 13 env-переменных, трижды проверенных по .conf-файлу. WireGuard-клиент отправляет handshake init через прокси. В логах прокси вижу: c->s: recv 148B, send 194B, junk=true. Пакет принят, трансформирован (148 + S1 = 194 байта), junk-пакеты отправлены. Всё по плану.

Жду handshake response.

Тишина.

Ни ответа, ни ошибки. WireGuard-клиент ждёт 5 секунд и ретранслирует handshake init. Прокси послушно трансформирует каждую ретрансляцию. Junk-пакеты летят. Трансформированные init'ы летят. Сервер - молчит. 5 секунд, 1, 3, минуты. WireGuard сдаётся: handshake did not complete. Ничего. Wireguard тихо молча отваливается по таймауту (и это внесло больше всего смуты).

Может, проблема в параметрах?

Первая мысль - я неправильно прочитал конфиг. Открываю .conf-файл, перепроверяю все параметры: H1-H4, S1, S2, Jc, Jmin, Jmax. Всё совпадает. Перепроверяю ещё раз, побуквенно. Совпадает.

Запускаю tcpdump на стороне сервера (благо, есть root-доступ). Пакеты приходят. Правильного размера: S1 + 148 = 194 байта. Перед ними - 4 junk-пакета в правильном диапазоне размеров (10-50 байт). Читаю hex-дамп, нахожу тип по смещению S1 - H1 на месте.

Три дня я перепроверял, методично исключая гипотезы:

  • Значения H1-H4 - три раза сверил с конфигом, конвертировал вручную в hex и сравнил с дампом

  • Размеры пакетов - ровно как ожидается, посчитал побайтово в Wireshark

  • Паддинг - случайные байты на месте, правильной длины, перед payload

  • Junk-пакеты - отправляются, правильного количества и размера

  • Endianness параметров - перепроверил, что H1 записывается как uint32 LE, а не BE

  • Сетевая связность - пинг до сервера проходит, UDP-порт открыт

  • Firewall - правила не блокируют, tcpdump на сервере видит пакеты

  • Реализовал даже упрощенную проксю для обычного AmneziaWG client, вдруг я реализовал udp-proxy криво. Но нет, родная amneziawg завелась успешно, значит проблема была в реализации.

Добавлял всё более детальное логирование. Выводил каждый байт входящего и исходящего пакета в hex. Сравнивал с тем, что показывает tcpdump. Всё совпадало (почти). Байт в байт. Пакет выходил из прокси точно таким, каким я его ожидал. Структура правильная. Но сервер его игнорировал.

Я начал подозревать баг в самом AWG-сервере. Пробовал подключиться обычным AmneziaWG-клиентом - работает. Значит, сервер исправен. Проблема в моём прокси. Но где?

Изучаем исходники

От безысходности полез копать исходники WireGuard - конкретно noise-protocol.c и cookie.c. И нашёл это:

При получении Handshake Init:
1. Проверить размер пакета           <- OK
2. Прочитать тип                     <- OK
3. Проверить MAC1                    <- !!!
4. Если MAC1 невалиден - DROP        <- молча, без логирования
5. Проверить MAC2 (если нужен)
6. Расшифровать static key
7. ...остальная обработка...

Хэш MAC1 проверяется ДО любой криптографической обработки пакета. Оказывается, это DoS-защита: проверка MAC1 дешёвая (один BLAKE2s-128), а расшифровка - дорогая. Если MAC1 невалиден, пакет отбрасывается немедленно. Без ответа. Без логирования. Молча. Это by design - нет смысла тратить ресурсы и раскрывать информацию о себе для пакетов с невалидным MAC.

MAC1 в Handshake Init - это:

mac1key = BLAKE2s-256("mac1" || server_public_key)
MAC1    = BLAKE2s-128(mac1key, packet[0:116])
                               ^^^^^^^^^^^^^^
                               включая type в bytes [0:4] !

Что делает прокси? Заменяет type с 1 на H1. Четыре байта. После замены, хэш MAC1, который MikroTik вычислил по type=1, становится невалидным для сервера, который теперь видит type=H1 в тех же 4 байтах.

MikroTik: MAC1 = BLAKE2s-128(key, [01,00,00,00 | rest...])   - вычислил
Прокси:   type = 01,00,00,00 -> 38,89,89,3D (H1)            - заменил
Сервер:   MAC1' = BLAKE2s-128(key, [38,89,89,3D | rest...])  - ожидает
          MAC1 != MAC1'                                       - DROP

Вот и проблема. 3 дня!

И это работает в обе стороны. Когда сервер отправляет Handshake Response с type=H2, прокси заменяет на type=2 и MikroTik WG-стек отбрасывает ответ по той же причине: MAC1 в ответе был вычислен по type=H2, а MikroTik ожидает MAC1 по type=2. Даже если бы сервер каким-то чудом ответил на пакет с невалидным MAC1 (что невозможно, но допустим) - MikroTik бы не принял его ответ. Двусторонний deadlock. Нужно переделывать расчёт MAC1.

В общем, пришлось реализовывать подсчет и пересчет MAC1 самостоятельно. А хотел обойтись "простой udp-прокси". С привлечением LLM'ки написал вполне сносный модуль, проверил, и.... запустилось! Пошли байтики в Tx и Rx, родненькие! Фух, на компе работает, осталось запустить это в Mikrotik'е. В подсчете MAC1 активно участвует публичный ключ туннеля, поэтому нужно прокинуть +1 env в контейнер.

В итоге, даже с учетом добавления небольшой криптографии с пересчетом MAC1, бинарники получились всего 806-866 кб! Это 0.85 МБ. Гораздо меньше ожидаемого! Отлично!

Развёртывание на MikroTik

Контейнеры MikroTik: ограничения

MikroTik RouterOS 7.4+ поддерживает Docker-контейнеры. Но это не полноценный Docker - скорее, минимальная реализация OCI runtime с существенными ограничениями:

  • RAM: контейнеры разделяют память роутера (обычно 256-512 МБ на всё). Каждый мбайт, съеденный контейнером - это МБ, отнятый у RouterOS.

  • Диск: NAND или eMMC, типично 128-256 МБ, из них свободного и того меньше. Образ контейнера хранится на flash.

  • Нет привычных docker-команд: ни тебе docker exec, ни docker pull, логи не посмотреть, установка вручную проприетарными командами вроде /container/add

  • Через winbox тоже не покликаешь - лично у меня типичный баг добавления контейнера в UI: это сделать по сути невозможно, т.к. бесконечно ругается на поле Shm size.

После нескольких часов тестов и дебагов - комплекс всё же завелся. Настройки задаю через переменные окружения, создаю дополнительный виртуальный интерфейс для контейнера, настраиваю маршрутизацию.

В процессе настройки я накопил себе целую кучу скриптов. Со временем я понял, что уже сам путаюсь что откуда брать, и представил как могут мучаться все остальные (или я сам через год, когда попытаюсь поднять новое соединение - уже всё забуду и буду не понимать, кто такое написал). Так что решил что надо это дело оформить в виде конфигуратора: вставляем туда конфиг подключения AmneziaWG.conf, он его парсит и выдает:

  • Команды на полную установку с проверками совместимости

  • Создает скрипт удаления всего этого - вдруг у вас что-то пойдёт не так и захотите откатиться

  • Конфигуратор помогает только установить базовое соединение. Он не настраивает вам маршрутизацию - это каждый делает сам. Если что - обращайтесь к LLM'кам

  • Не забывайте создавать бэкапы перед выполнением команд!

В процессе эксплуатации выявил, что ЦПУ в основном потребляет Wireguard. Контейнер awg-proxy потребляет ЦПУ незначительно (1-2%). Бинарник хоть и весит 0.9 МБ, но потребление Ram колеблется в районе 7-10 МБ. Глобально, у меня 128 МБ, я могу себе это позволить)

Итоги

Ограничения рождают креативность. Когда совсем уже припекло, родилось хорошее решение переиспользовать. По итогу был реализован минималистичный AmneziaWG-container, неплохо (уверен, можно сделать ещё лучше) решающий мою задачу, делюсь с обществом (для ознакомления, см. disclaimer).

Github (MIT) копия на gitlab

Конфигуратор, копия gitlab