Не люблю, когда сервер настраивает за меня чёрный ящик — пусть даже удобный. Приложение AmneziaVPN разворачивает AmneziaWG само: заходит по SSH, ставит модуль, генерит конфиги, рисует QR-код. Но что именно происходит на сервере — не видно, и в свою автоматизацию такое не воткнёшь. Поэтому поднимаю руками, шаг за шагом, с пониманием, что где происходит. Дальше сам решаешь: оставить как обычный systemd-сервис, обернуть в свой скрипт или утащить движок в собственное приложение.
Протокол — форк WireGuard от команды Amnezia, прячущий характерные сигнатуры WireGuard от DPI. Крипто-ядро не тронуто (тот же Noise_IK, Curve25519, ChaCha20-Poly1305) — вся обфускация на транспортном уровне: мусорные пакеты перед хендшейком, паддинг заголовков, а с версии 2.0 — постоянная мимикрия под QUIC/DNS/SIP прямо во время передачи данных, а не только в момент подключения.
Развилка сразу в начале. Гонять протокол можно двумя способами:
Если сомневаюсь, что у меня за виртуализация — проверяю:
Дальше по тексту — путь с модулем ядра, он основной. Userspace — отдельным блоком внутри шага 2.
Апгрейд может притащить новое ядро. Модуль собирается строго под то ядро, которое реально запущено — поэтому ребут делаю до сборки, не после. Собери модуль под старое ядро, потом обнови систему — и модуль тихо перестанет грузиться при следующей загрузке. Поломка всплывёт недели через две, когда про этот сервер уже забудешь.
После ребута:
Создам пользователя, заменю порт ssh и запрещу вход по root, настрою ufw.
На современных ядрах (Ubuntu 24.04+/Debian 12+, 6.8 и новее) в большинстве случаев уже собирается на одних заголовках. Если нет —
Если
Строчка в
Нужен Go:
(если в репозитории старая версия — ставлю актуальную с go.dev вручную,
Дальше все шаги (конфиг,
Никаких зависимостей кроме компилятора и libc. Ставится
Прежде чем писать конфиг — правило, которое ловит 90% случаев "тоннель поднялся, а хендшейка нет": из параметров обфускации между клиентом и сервером обязаны совпадать S1-S4 и H1-H4. А
Насчёт верхней границы H1-H4: спецификация допускает весь диапазон uint32 (до 4294967295), но у
Не беру первые попавшиеся "круглые" числа (1, 2, 3, 4 — такое реально гуляет по примерам в интернете): чем более случайно выглядят значения, тем меньше шанс, что мой сервер совпадёт по отпечатку с чужим, настроенным по тому же туториалу.
Генератор разбит на
Тут же в скрипте уже сидят add-client/remove-client (шаг 10) и опциональная поддержка I1-I5 (шаг 11) — не задаёшь AWG_I1..AWG_I5, эти строки просто не появляются в выводе block.
Для клиента на шаге 8 не переписываю эти восемь чисел заново — зову
Дальше смотрю имя внешнего сетевого интерфейса (почти наверняка не
И сам конфиг (порт и подсеть — свои, ниже мой пример;
Порт беру не 51820 (стандартный WireGuard-порт по умолчанию) — сам протокол это не разоблачит, но незачем облегчать жизнь эвристикам лишний раз задаром.
Открываю порт:
Добавляю пира на сервер и применяю без разрыва тоннеля (не
Конфиг для самого клиента. За S1-S4/H1-H4 не сажусь вспоминать или пересчитывать заново — это и есть тот самый способ незаметно развести клиента с сервером. Зову ровно ту же
Jc/Jmin/Jmax в выводе — новые (и так и надо), S1-S4/H1-H4 — побитово те же, что в серверном конфиге, потому что взяты из одного
пример рабочего конфига
сохраняю как файл с расширением .conf для windows
Без DNS при полном туннеле (AllowedIPs = 0.0.0.0/0) клиент продолжит резолвить через то, что было настроено до поднятия тоннеля — либо DNS-лик мимо тоннеля, либо резолвер вообще станет недостижим, если он был в локальной подсети, которую перекрыл маршрут 0.0.0.0/0. Любой публичный резолвер подойдёт, 1.1.1.1 тут просто пример.
Не считаю зелёный
Смотрю на
С клиента — что трафик реально идёт через тоннель, а не просто "интерфейс есть":
Должен показать IP сервера, а не свой.
Сомневаюсь в самой обфускации — смотрю глазами, что летит по проводу:
На классический WireGuard-хендшейк это выглядеть не должно.
Ручной путь из шага 8 (сгенерировать ключи, дописать [Peer], применить syncconf) стоит пройти один раз руками — чтобы понимать, что происходит внутри. Дальше для каждого нового устройства или пользователя быстрее и безопаснее через sc.sh: он не даст S1-S4/H1-H4 разъехаться (см. шаг 5 — и на своей шкуре в этом гайде), сам находит свободный IP и сам применяет изменение без разрыва тоннеля.
Добавить:
Находит следующий свободный IP в подсети, генерит новую пару ключей, дописывает в awg0.conf блок [Peer] с меткой
Удалить:
Убирает [Peer]-блок по метке, применяет syncconf (пир исчезает из рантайма немедленно, не только из файла на бумаге), стирает каталог с его ключами. Имени нет — откажет с понятным сообщением, файл не тронет.
Оба вызова — с одним sudo на всю команду, не вложенным (та же причина, что в шаге 8: awg/awg-quick внутри sc.sh уже не дублируют sudo сами).
Всё, что было до сих пор (H1-H4, S1-S4, Jc/Jmin/Jmax) — прячет отпечаток WireGuard, но не делает трафик похожим на что-то конкретное. Для DPI, который ищет сигнатуру WireGuard, — этого достаточно. Для DPI, который блокирует всё неопознанное независимо от сигнатуры, — не совсем: набор случайных байт без опознаваемой формы сам по себе может быть подозрителен.
I1-I5 — до пяти UDP-пакетов, отправляемых перед каждым хендшейком (а он повторяется раз в 120 секунд, не только при первом подключении), которые по заданному формату CPS могут выглядеть как настоящий QUIC/DNS/SIP или другой UDP-протокол. I1 обычно несёт байтовый слепок реального протокола, I2-I5 добавляют энтропию — счётчики, таймстемпы, случайные данные, — чтобы слепок не повторялся один в один при каждом хендшейке.
Что это реально даёт, а что нет. I1-I5 меняют то, как выглядит исходящий пакет для пассивного наблюдателя — не более того. Сервер на эти пакеты никак не отвечает и не умеет по-настоящему говорить на QUIC/DNS/SIP: если DPI не просто слушает, а активно зондирует порт, I1-I5 сам по себе такую проверку не пройдёт. Для ответа на активное зондирование нужна отдельная надстройка перед тоннелем — например, wiresock/amneziawg-proxy, который действительно отвечает на QUIC/DNS/STUN/SIP-пробы как настоящий сервис. Это отдельный инструмент, не то же самое, что I1-I5.
Официальный способ получить настоящий слепок — через Wireshark: ставишь фильтр на нужный протокол (quic, dns...), ловишь реальный пакет, копируешь его hex-байты в
Более быстрый путь без Wireshark — готовые общественные наборы (гуглится список VoidWaifu) или генератор конкретно под QUIC с нужным SNI. Это не праздный выбор: у отдельных хостеров (Hetzner — самый известный случай) в 2026 всплыла блокировка на уровне AS целиком — хендшейк проходит, а данные потом не идут. Помогает именно I1 с SNI, которому конкретно этот провайдер/эта сеть доверяет, а не универсальный пример; какой SNI сработает — заранее не угадать, дело подбора под конкретную сеть.
Синтаксис тегов:
Можно комбинировать в одном параметре:
Две вещи, которые ловятся молча, если забыть:
Как включить через sc.sh:
Сервер уже проинициализирован без I1-I5 (см. шаг 5) — init не перезапишет существующий файл параметров, допиши I1=... в awg-obfuscation.env вручную той же строкой и перезапусти сервис. Для отдельного клиента с другим слепком, отличным от сохранённого в файле, передай переменную прямо перед вызовом block/add-client:
Полноценная обфускация AWG 2.0 (заголовки, паддинг, мимикрия I1-I5) уже собрана — не хуже того, что накатывает приложение. Дальше по желанию:
Ссылки на скачивание клиента:
Windows 10,11
MacOS 13
MacOS 12
MacOS 10,11
iOS и альтернатива
Android
Linux
GitHub
И краткая инструкция по автоматической настройке:
Берем нужную версию программы для своей системы по ссылке выше:
Далее устанавливаем и запускаем
нажимаем кнопку + и выбираем self-hosted
далее ввожу данные своего купленного сервера
где 127.0.0.1 мой ip адрес
22 это порт по умолчанию
root логин
и пароль
и нажимаю Continue продолжить
далее снова продолжаю так как меня интересует автоматическая установка, я ничего не трогаю
процесс пошел
Через 5 мин. будет готов личный vpn сервер
Далее после установки на главном экране увидим наш сервер с кнопкой Connect для подключения.
На этом все, получение личного впн завершено
Протокол — форк WireGuard от команды Amnezia, прячущий характерные сигнатуры WireGuard от DPI. Крипто-ядро не тронуто (тот же Noise_IK, Curve25519, ChaCha20-Poly1305) — вся обфускация на транспортном уровне: мусорные пакеты перед хендшейком, паддинг заголовков, а с версии 2.0 — постоянная мимикрия под QUIC/DNS/SIP прямо во время передачи данных, а не только в момент подключения.
Что нужно
- Ubuntu 24.04/26.04 LTS или Debian 12/13 — root или sudo по SSH
- один свободный UDP-порт наружу
- минут 20-30
Развилка сразу в начале. Гонять протокол можно двумя способами:
- Модуль ядра (
amneziawg-linux-kernel-module) — максимальная производительность, крипто считается в ядре. Беру его на обычной VPS (KVM/Xen) или железе. В LXC/OpenVZ-контейнере не заведётся — там нет доступа к сборке своего модуля ядра. - Userspace (
amneziawg-go) — работает где угодно, включая контейнер, но крипто считается в user space — заметно больше нагрузки на CPU при высоком трафике.
Если сомневаюсь, что у меня за виртуализация — проверяю:
systemd-detect-virt. Ответ kvm, none (железо) или xen — беру модуль. lxc, openvz, docker — сразу к userspace-варианту в шаге 2.Дальше по тексту — путь с модулем ядра, он основной. Userspace — отдельным блоком внутри шага 2.
Шаг 1. Готовлю систему
Bash:
sudo apt update && sudo apt full-upgrade -y
sudo reboot
Апгрейд может притащить новое ядро. Модуль собирается строго под то ядро, которое реально запущено — поэтому ребут делаю до сборки, не после. Собери модуль под старое ядро, потом обнови систему — и модуль тихо перестанет грузиться при следующей загрузке. Поломка всплывёт недели через две, когда про этот сервер уже забудешь.
После ребута:
Создам пользователя, заменю порт ssh и запрещу вход по root, настрою ufw.
Bash:
sudo apt install -y build-essential dkms linux-headers-$(uname -r) git
build-essential— компилятор (gcc) и make, без них ничего не соберётсяdkms(Dynamic Kernel Module Support) — прослойка, которая сама пересобирает модуль при каждом обновлении ядра в будущем. Без неё любойapt upgradeс новым ядром тихо убьёт тоннельlinux-headers-$(uname -r)— заголовки именно под текущее ядро (поэтому порядок важен: сначала ребут, потом эта команда —uname -rдолжен показывать то ядро, которое реально будет работать)
Шаг 2. Модуль ядра amneziawg
Bash:
cd /usr/src
sudo git clone https://github.com/amnezia-vpn/amneziawg-linux-kernel-module.git
cd amneziawg-linux-kernel-module/src
sudo make dkms-install
sudo dkms add -m amneziawg -v 1.0.0
sudo dkms build -m amneziawg -v 1.0.0
На современных ядрах (Ubuntu 24.04+/Debian 12+, 6.8 и новее) в большинстве случаев уже собирается на одних заголовках. Если нет —
dkms build падает с текстом вроде:
Код:
You're running a modern Linux Kernel (version X.X.X).
In order to build AmneziaWG kernel module for this kernel
you must obtain sources of your kernel by yourself...
Если увидел эту ошибку написал:
Если
dkms build прошёл — ставлю и гружу:
Bash:
sudo dkms install -m amneziawg -v 1.0.0
echo amneziawg | sudo tee /etc/modules-load.d/amneziawg.conf
sudo modprobe amneziawg
lsmod | grep amneziawg
Строчка в
/etc/modules-load.d/ — чтобы модуль подгружался сам при каждой загрузке. Без неё после ребута тоннель не поднимется, пока не сделаю modprobe руками.Secure Boot написал:
Альтернатива: userspace (amneziawg-go)
Нужен Go:
Bash:
sudo apt install -y golang-go
(если в репозитории старая версия — ставлю актуальную с go.dev вручную,
amneziawg-go собирается любой достаточно свежей версией)
Bash:
cd ~
git clone https://github.com/amnezia-vpn/amneziawg-go.git
cd amneziawg-go
make
sudo cp amneziawg-go /usr/bin/amneziawg-go
Дальше все шаги (конфиг,
awg-quick, systemd) идентичны — awg-quick сам использует userspace-движок, если модуль ядра не загружен.Шаг 3. Инструменты awg / awg-quick
Bash:
cd ~
git clone https://github.com/amnezia-vpn/amneziawg-tools.git
cd amneziawg-tools/src
make
sudo make install
Никаких зависимостей кроме компилятора и libc. Ставится
awg (аналог wg) и awg-quick (аналог wg-quick), man-страницы и — раз в системе есть systemd — юнит awg-quick@.service.Шаг 4. Ключи
Bash:
sudo mkdir -p /etc/amnezia/amneziawg
cd /etc/amnezia/amneziawg
sudo bash -c '
umask 077
awg genkey | tee server_private.key | awg pubkey > server_public.key
awg genpsk > preshared_pool.key
'
umask 077 — чтобы ключи создавались без прав на чтение для кого угодно кроме root. server_private.key с сервера никуда не уходит. server_public.key — то, что раздаю пирами. Preshared key — дополнительный симметричный слой поверх обычного хендшейка Noise_IK, в классическом WireGuard опционален, но это одна строчка и бесплатная подушка безопасности на случай если с эллиптическими кривыми что-то случится в будущем.Шаг 5. Серверный конфиг
Прежде чем писать конфиг — правило, которое ловит 90% случаев "тоннель поднялся, а хендшейка нет": из параметров обфускации между клиентом и сервером обязаны совпадать S1-S4 и H1-H4. А
Jc/Jmin/Jmax у каждой стороны свои — это мусорные пакеты, данных не несут, каждая сторона генерит их независимо. I1-I5 (см. шаг 11) — отдельный случай: содержимое тоже может различаться, но включены они должны быть на обеих сторонах одновременно, либо нигде.- Jc — сколько мусорных пакетов слать перед хендшейком. 1–128, рекомендую 4–12. Совпадение не нужно.
- Jmin/Jmax — размер мусорных пакетов в байтах. Jmin < Jmax < 1280. Совпадение не нужно.
- S1 — паддинг Handshake Init. ≤ 1132, и S1+56 ≠ S2. Должен совпадать.
- S2 — паддинг Handshake Response. ≤ 1188. Должен совпадать.
- S3 — паддинг Cookie Reply, AWG 2.0. 0–64. Должен совпадать.
- S4 — паддинг каждого пакета данных, AWG 2.0. 0–32. Должен совпадать.
- H1-H4 — подмена магических заголовков типов пакетов. Уникальны между собой, 5–2147483647. Должны совпадать.
- I1-I5 — опциональные пакеты-приманки под протокол (шаг 11). CPS-строка, регистр важен (I1, не i1). Содержимое — нет, присутствие — да.
Насчёт верхней границы H1-H4: спецификация допускает весь диапазон uint32 (до 4294967295), но у
amneziawg-windows-client есть открытый баг — значения выше 2147483647 (INT32_MAX) подсвечиваются как невалидные и могут не сохраниться. Держу H-значения под этим потолком, даже когда формально можно больше — просто чтобы не отстрелить себе Windows-клиентов.Не беру первые попавшиеся "круглые" числа (1, 2, 3, 4 — такое реально гуляет по примерам в интернете): чем более случайно выглядят значения, тем меньше шанс, что мой сервер совпадёт по отпечатку с чужим, настроенным по тому же туториалу.
Генератор разбит на
init/block, а не просто печатает числа при каждом запуске: если гонять его отдельно для сервера и отдельно для каждого клиента, $RANDOM каждый раз даёт новый набор — S1-S4/H1-H4 у клиента и сервера тихо разъедутся, хендшейка не будет вообще, и ни одной ошибки в логе, чтобы это поймать (проверено на себе). init считает восемь чисел один раз и кладёт в файл, block каждый раз читает их оттуда и добавляет свежие Jc/Jmin/Jmax:
Bash:
cat > sc.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${AWG_BASE_DIR:-/etc/amnezia/amneziawg}"
PARAMS_FILE="${AWG_PARAMS_FILE:-$BASE_DIR/awg-obfuscation.env}"
CONF_FILE="${AWG_CONF_FILE:-$BASE_DIR/awg0.conf}"
CLIENTS_DIR="${AWG_CLIENTS_DIR:-$BASE_DIR/clients}"
IFACE="${AWG_IFACE:-awg0}"
SUBNET_PREFIX="${AWG_SUBNET_PREFIX:-10.29.29}"
rand_range() { echo $(( $1 + RANDOM % ($2 - $1 + 1) )); }
big_rand() { echo $(( RANDOM * 32768 + RANDOM + 1000 )); }
cmd_init() {
[ -f "$PARAMS_FILE" ] && {
echo "Уже есть $PARAMS_FILE — не трогаю." >&2
echo "Удали руками, если реально хочешь перегенерировать (отвяжет текущих клиентов)." >&2
exit 1
}
local S1 S2 S3 S4 H1 H2 H3 H4
S1=$(rand_range 15 150)
S2=$(rand_range 15 150)
while [ "$S2" -eq "$((S1 + 56))" ]; do S2=$(rand_range 15 150); done
S3=$(rand_range 0 64)
S4=$(rand_range 0 32)
H1=$(big_rand)
H2=$(big_rand); while [ "$H2" -eq "$H1" ]; do H2=$(big_rand); done
H3=$(big_rand); while [ "$H3" -eq "$H1" ] || [ "$H3" -eq "$H2" ]; do H3=$(big_rand); done
H4=$(big_rand); while [ "$H4" -eq "$H1" ] || [ "$H4" -eq "$H2" ] || [ "$H4" -eq "$H3" ]; do H4=$(big_rand); done
mkdir -p "$(dirname "$PARAMS_FILE")"
{
echo "S1=$S1"; echo "S2=$S2"; echo "S3=$S3"; echo "S4=$S4"
echo "H1=$H1"; echo "H2=$H2"; echo "H3=$H3"; echo "H4=$H4"
# %q обязателен: CPS-значения содержат < >, а файл ниже source-ится как bash-скрипт
[ -n "${AWG_I1:-}" ] && printf 'I1=%q\n' "$AWG_I1"
[ -n "${AWG_I2:-}" ] && printf 'I2=%q\n' "$AWG_I2"
[ -n "${AWG_I3:-}" ] && printf 'I3=%q\n' "$AWG_I3"
[ -n "${AWG_I4:-}" ] && printf 'I4=%q\n' "$AWG_I4"
[ -n "${AWG_I5:-}" ] && printf 'I5=%q\n' "$AWG_I5"
} > "$PARAMS_FILE"
chmod 600 "$PARAMS_FILE"
echo "Сохранено в $PARAMS_FILE (один раз на весь сервер)" >&2
}
cmd_block() {
[ -f "$PARAMS_FILE" ] || { echo "Нет $PARAMS_FILE. Сначала: $0 init" >&2; exit 1; }
local S1 S2 S3 S4 H1 H2 H3 H4 JC JMIN JMAX I1 I2 I3 I4 I5
I1=""; I2=""; I3=""; I4=""; I5=""
source "$PARAMS_FILE"
I1="${AWG_I1:-$I1}"; I2="${AWG_I2:-$I2}"; I3="${AWG_I3:-$I3}"
I4="${AWG_I4:-$I4}"; I5="${AWG_I5:-$I5}"
JC=$(rand_range 4 12)
JMIN=$(rand_range 8 60)
JMAX=$(rand_range $((JMIN + 50)) 1200)
cat <<BLOCK
Jc = $JC
Jmin = $JMIN
Jmax = $JMAX
S1 = $S1
S2 = $S2
S3 = $S3
S4 = $S4
H1 = $H1
H2 = $H2
H3 = $H3
H4 = $H4
BLOCK
[ -n "$I1" ] && echo "I1 = $I1"
[ -n "$I2" ] && echo "I2 = $I2"
[ -n "$I3" ] && echo "I3 = $I3"
[ -n "$I4" ] && echo "I4 = $I4"
[ -n "$I5" ] && echo "I5 = $I5"
return 0
}
cmd_add_client() {
local name="${1:?Использование: $0 add-client <имя>}"
local clientdir="$CLIENTS_DIR/$name"
[ -f "$PARAMS_FILE" ] || { echo "Нет $PARAMS_FILE. Сначала: $0 init" >&2; exit 1; }
[ -f "$CONF_FILE" ] || { echo "Нет $CONF_FILE" >&2; exit 1; }
[ -f "$BASE_DIR/server_public.key" ] || { echo "Нет $BASE_DIR/server_public.key" >&2; exit 1; }
if grep -q "^# client: ${name}\$" "$CONF_FILE" 2>/dev/null; then
echo "Клиент '$name' уже есть в $CONF_FILE" >&2; exit 1
fi
if [ -d "$clientdir" ]; then echo "Каталог $clientdir уже существует" >&2; exit 1; fi
local next_octet
next_octet=$(grep -oP "AllowedIPs\s*=\s*${SUBNET_PREFIX//./\\.}\.\K[0-9]+" "$CONF_FILE" | sort -n | tail -1 || true)
next_octet=$((${next_octet:-1} + 1))
if [ "$next_octet" -gt 254 ]; then echo "Подсеть ${SUBNET_PREFIX}.0/24 исчерпана" >&2; exit 1; fi
local server_port
server_port=$(grep -oP '^ListenPort\s*=\s*\K[0-9]+' "$CONF_FILE" | head -1 || true)
[ -n "$server_port" ] || { echo "Не нашёл ListenPort в $CONF_FILE — конфиг повреждён?" >&2; exit 1; }
mkdir -p "$clientdir"
chmod 700 "$clientdir"
(
umask 077
awg genkey | tee "$clientdir/private.key" | awg pubkey > "$clientdir/public.key"
awg genpsk > "$clientdir/preshared.key"
)
local client_pub client_psk
client_pub=$(cat "$clientdir/public.key")
client_psk=$(cat "$clientdir/preshared.key")
{
echo ""
echo "# client: ${name}"
echo "[Peer]"
echo "PublicKey = ${client_pub}"
echo "PresharedKey = ${client_psk}"
echo "AllowedIPs = ${SUBNET_PREFIX}.${next_octet}/32"
} >> "$CONF_FILE"
awg syncconf "$IFACE" <(awg-quick strip "$CONF_FILE")
{
echo "[Interface]"
echo "PrivateKey = $(cat "$clientdir/private.key")"
echo "Address = ${SUBNET_PREFIX}.${next_octet}/24"
echo "DNS = ${AWG_DNS:-1.1.1.1}"
cmd_block
echo ""
echo "[Peer]"
echo "PublicKey = $(cat "$BASE_DIR/server_public.key")"
echo "PresharedKey = ${client_psk}"
echo "Endpoint = ${AWG_ENDPOINT:-СЮДА_IP_И_ПОРТ_СЕРВЕРА}"
echo "AllowedIPs = 0.0.0.0/0"
echo "PersistentKeepalive = 25"
} > "$clientdir/${name}.conf"
echo "Добавлен '${name}': ${SUBNET_PREFIX}.${next_octet}, конфиг — $clientdir/${name}.conf" >&2
}
cmd_remove_client() {
local name="${1:?Использование: $0 remove-client <имя>}"
local clientdir="$CLIENTS_DIR/$name"
local start end total prev_line
start=$(grep -n "^# client: ${name}\$" "$CONF_FILE" | head -1 | cut -d: -f1 || true)
if [ -z "$start" ]; then echo "Клиент '$name' не найден в $CONF_FILE" >&2; exit 1; fi
total=$(wc -l < "$CONF_FILE")
end=$(tail -n +"$((start + 1))" "$CONF_FILE" | grep -n "^# client: " | head -1 | cut -d: -f1 || true)
if [ -n "$end" ]; then end=$((start + end - 1)); else end=$total; fi
if [ "$start" -gt 1 ]; then
prev_line=$(sed -n "$((start - 1))p" "$CONF_FILE")
[ -z "$prev_line" ] && start=$((start - 1))
fi
sed -i "${start},${end}d" "$CONF_FILE"
awg syncconf "$IFACE" <(awg-quick strip "$CONF_FILE")
rm -rf "$clientdir"
echo "Клиент '${name}' удалён и из конфига, и из рантайма ($start-$end)" >&2
}
case "${1:-}" in
init) cmd_init ;;
block) cmd_block ;;
add-client) shift; cmd_add_client "$@" ;;
remove-client) shift; cmd_remove_client "$@" ;;
*)
cat <<USAGE >&2
Использование: $0 {init|block|add-client <имя>|remove-client <имя>}
init — один раз на сервер: генерит и сохраняет S1-S4/H1-H4 (+опционально I1-I5 из AWG_I1..AWG_I5)
block — печатает блок обфускации (S1-S4/H1-H4/I1-I5 из файла, Jc/Jmin/Jmax свежие; AWG_I1..AWG_I5 переопределяют на один вызов)
add-client NAME — новый пир: ключи, следующий свободный IP, live-применение, готовый .conf
remove-client NAME — отзыв пира: убирает из конфига, из рантайма, чистит ключи
USAGE
exit 1
;;
esac
SCRIPT
chmod +x sc.sh
./sc.sh init
./sc.sh block > /tmp/awg-server-block.txt
cat /tmp/awg-server-block.txt
Тут же в скрипте уже сидят add-client/remove-client (шаг 10) и опциональная поддержка I1-I5 (шаг 11) — не задаёшь AWG_I1..AWG_I5, эти строки просто не появляются в выводе block.
$RANDOM в bash отдаёт только 0-32767 — для маленьких диапазонов (Jc, S1-S4) этого хватает, а для H-шек (нужны значения до ~2 млрд) пришлось скомбинировать два вызова в big_rand. Криптостойкая случайность тут не нужна — эти параметры не секрет, важно лишь не повторяться с другими читателями одного туториала.Для клиента на шаге 8 не переписываю эти восемь чисел заново — зову
./sc.sh block ещё раз: S1-S4/H1-H4 придут те же самые (из файла), Jc/Jmin/Jmax — новые.Дальше смотрю имя внешнего сетевого интерфейса (почти наверняка не
eth0, особенно на облаке):
Bash:
ip -brief address
И сам конфиг (порт и подсеть — свои, ниже мой пример;
SERVER_IFACE подставляю из команды выше):
Bash:
SERVER_PORT=44847
SERVER_IFACE=ens3
sudo bash -c "cat > /etc/amnezia/amneziawg/awg0.conf" <<EOF
[Interface]
PrivateKey = $(sudo cat /etc/amnezia/amneziawg/server_private.key)
Address = 10.29.29.1/24
ListenPort = $SERVER_PORT
$(cat /tmp/awg-server-block.txt)
PostUp = iptables -t nat -A POSTROUTING -o $SERVER_IFACE -j MASQUERADE; iptables -A FORWARD -i awg0 -j ACCEPT; iptables -A FORWARD -o awg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o $SERVER_IFACE -j MASQUERADE; iptables -D FORWARD -i awg0 -j ACCEPT; iptables -D FORWARD -o awg0 -j ACCEPT
EOF
sudo chmod 600 /etc/amnezia/amneziawg/awg0.conf
Порт беру не 51820 (стандартный WireGuard-порт по умолчанию) — сам протокол это не разоблачит, но незачем облегчать жизнь эвристикам лишний раз задаром.
Шаг 6. Форвардинг и файрвол
Bash:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-amneziawg.conf
sudo sysctl --system
Открываю порт:
Bash:
sudo ufw allow $SERVER_PORT/udp # если использую ufw
Про ufw и файрвол провайдера написал:
Шаг 7. Запуск
Bash:
sudo systemctl enable --now awg-quick@awg0
sudo systemctl status awg-quick@awg0
enable, а не только start — иначе не переживёт следующую перезагрузку.Шаг 8. Клиент
Bash:
sudo bash -c '
cd /etc/amnezia/amneziawg
umask 077
awg genkey | tee client1_private.key | awg pubkey > client1_public.key
awg genpsk > client1_preshared.key
'
Добавляю пира на сервер и применяю без разрыва тоннеля (не
restart, а syncconf — так уже подключённых клиентов не дёргаю):
Bash:
sudo bash -c "cat >> /etc/amnezia/amneziawg/awg0.conf" <<EOF
[Peer]
PublicKey = $(sudo cat /etc/amnezia/amneziawg/client1_public.key)
PresharedKey = $(sudo cat /etc/amnezia/amneziawg/client1_preshared.key)
AllowedIPs = 10.29.29.2/32
EOF
sudo bash -c 'awg syncconf awg0 <(awg-quick strip /etc/amnezia/amneziawg/awg0.conf)'
Важно про sudo написал:
Конфиг для самого клиента. За S1-S4/H1-H4 не сажусь вспоминать или пересчитывать заново — это и есть тот самый способ незаметно развести клиента с сервером. Зову ровно ту же
sc.sh, что и на шаге 5 (awg-obfuscation.env уже лежит рядом со скриптом):
Bash:
./sc.sh block > /tmp/awg-client1-block.txt
cat /tmp/awg-client1-block.txt
Jc/Jmin/Jmax в выводе — новые (и так и надо), S1-S4/H1-H4 — побитово те же, что в серверном конфиге, потому что взяты из одного
awg-obfuscation.env. Вставляю как есть:
Код:
[Interface]
PrivateKey = <client1_private.key>
Address = 10.29.29.2/24
DNS = 1.1.1.1
Jc = ... # из вывода sc.sh block выше
Jmin = ...
Jmax = ...
S1 = ...
S2 = ...
S3 = ...
S4 = ...
H1 = ...
H2 = ...
H3 = ...
H4 = ...
[Peer]
PublicKey = <server_public.key>
PresharedKey = <client1_preshared.key>
Endpoint = Мой_ip_сервера:44847
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
пример рабочего конфига
Код:
[Interface]
PrivateKey = SKj/BQe0g8ZiutigYR5UaoMxWGLlJmEHhr29vJwYJkg=
Address = 10.29.29.3/24
DNS = 1.1.1.1
Jc = 10
Jmin = 32
Jmax = 149
S1 = 88
S2 = 103
S3 = 38
S4 = 2
H1 = 936817872
H2 = 341090999
H3 = 415596440
H4 = 449719160
[Peer]
PublicKey = 4I2R5c1okFahouusWTkYMS8ZxGwRpcOGTuSXKH5AphQ=
PresharedKey = wyOv/lk4GPRRjaDo+TE0mqonjtagwsiH38jZCFI5Fu4=
Endpoint = 127.0.0.1:44847
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
сохраняю как файл с расширением .conf для windows
Без DNS при полном туннеле (AllowedIPs = 0.0.0.0/0) клиент продолжит резолвить через то, что было настроено до поднятия тоннеля — либо DNS-лик мимо тоннеля, либо резолвер вообще станет недостижим, если он был в локальной подсети, которую перекрыл маршрут 0.0.0.0/0. Любой публичный резолвер подойдёт, 1.1.1.1 тут просто пример.
PersistentKeepalive ставлю на клиенте (он почти всегда за NAT и должен сам держать маппинг открытым), на сервере не нужен. Для мобильного импорта одним сканом:
Bash:
sudo apt install -y qrencode
qrencode -t ansiutf8 < client1.conf
Шаг 9. Чем подключаюсь, если не AmneziaVPN
- Другой Linux/macOS — те же шаги 2-3, потом
awg-quick up client1.conf. - Телефон/десктоп без сборки из исходников — официальные, но отдельные от AmneziaVPN клиенты: Android (Google Play,
org.amnezia.awg), iOS (App Store, «AmneziaWG»), Windows (GitHub-релизыamneziawg-windows-client). Импортирую тот же .conf. - Ванильный клиент WireGuard не подойдёт — обфускация зашита в формат пакетов, а не просто в конфиг-опцию, так что стоковый WG-клиент такой хендшейк просто не поймёт.
- Роутер — для OpenWRT есть готовые ipk-пакеты (
kmod-amneziawg,amneziawg-tools,luci-app-amneziawg), у Keenetic — нативная поддержка asc-параметров в UI, для MikroTik — через контейнер. Отдельная тема, за рамками этой статьи.
Проверяю, что реально работает
Не считаю зелёный
systemctl status доказательством — это только значит, что интерфейс поднялся, а не что кто-то через него прошёл.
Bash:
sudo awg show awg0
Смотрю на
latest handshake — должно быть недавнее время после подключения клиента, не пусто. И на transfer — счётчики должны расти при повторном запуске команды во время активной сессии, а не стоять на месте.С клиента — что трафик реально идёт через тоннель, а не просто "интерфейс есть":
Bash:
curl -4 ifconfig.me
Должен показать IP сервера, а не свой.
Сомневаюсь в самой обфускации — смотрю глазами, что летит по проводу:
Bash:
sudo tcpdump -i $SERVER_IFACE -n udp port $SERVER_PORT
На классический WireGuard-хендшейк это выглядеть не должно.
Если не взлетело
- dkms build падает с "must obtain sources of your kernel" — см. шаг 2: либо тяну полный сорцтри ядра по официальному README, либо (проще и надёжнее) перехожу на userspace amneziawg-go на этой машине.
- Модуль не грузится, "Key was rejected" — Secure Boot в UEFI: отключаю в панели провайдера или подписываю модуль через MOK.
- Хендшейка нет вообще, ни ошибок в логах — в 9 случаях из 10 не совпали S1-S4/H1-H4 между клиентом и сервером. Реже — порт зарублен файрволом провайдера ещё до сервера.
- Клиент виснет на "Connecting...", awg show на сервере не показывает ни latest handshake, ни endpoint у пира — классический случай предыдущего пункта: генератор параметров запускали дважды по отдельности (для сервера и для клиента), $RANDOM каждый раз даёт новый набор. Сверяю S1-S4/H1-H4 построчно (
grep -E '^(S[1-4]|H[1-4]) =' awg0.confна обеих сторонах); при использовании sc.sh из шага 5 это исключено by design — значения берутся из одного файла. - Модуль работал, после ребута — нет — забыл /etc/modules-load.d/amneziawg.conf, либо enable вместо просто start.
- Хендшейк есть, интернета через тоннель нет — забыл ip_forward в sysctl, либо неверный интерфейс в PostUp/MASQUERADE, либо ufw режет FORWARD.
- Windows-клиент подсвечивает H-значение как invalid — значение выше 2147483647, известный баг amneziawg-windows-client — держу H1-H4 под этим потолком.
- awg syncconf падает с fopen: No such file or directory — два вложенных sudo вокруг <(...): внешний закрывает файловый дескриптор раньше, чем awg успевает его открыть. Оборачиваю всю команду в один sudo bash -c '...', либо убираю sudo, если уже root.
- add-client/remove-client не находятся, вместо результата — старое сообщение об использовании без этих команд — на сервере лежит более старая версия sc.sh, чем та, что редактировал только что: правка не долетела до файла. Проверяю:
grep -c "add-client" sc.sh(0 — точно старая версия) или сверяю md5sum с эталоном. - Добавил I1, хендшейк пропал именно после этого — скорее всего опечатка в регистре (i1 вместо I1) на одной из сторон: та сторона тихо остаётся без мимикрии, другая ждёт AWG 2.0, хендшейк не сходится. Либо I1 есть только на одной стороне — включать/выключать надо на обеих сразу (см. шаг 11).
- Взял пример I1 прямо из документации Amnezia, ничего не изменилось к лучшему — ожидаемо: в документации прямым текстом написано не использовать их иллюстрацию как реальное значение. Общий пример, скопированный всеми читателями одного гайда, перестаёт быть уникальным сам по себе.
- add-client создаёт клиентский .conf, обрывающийся сразу на H4 — без [Peer] вообще — баг в sc.sh версий до этого исправления: cmd_block заканчивалась проверкой [ -n "$I5" ] && echo ..., и если I1-I5 не заданы (обычный случай), это последнее выражение ложно — функция возвращает ненулевой код, а set -e тихо обрывает сборку файла сразу после вызова cmd_block, до строк [Peer]. Обнови sc.sh из этого гайда (там уже стоит return 0 в конце cmd_block) — уже созданные обрезанные конфиги можно дособрать вручную: [Peer]-секция для них уже реально применена на сервере (это отдельный, более ранний шаг в add-client), не хватает только хвоста в файле клиента.
Шаг 10. Добавляю и удаляю клиентов
Ручной путь из шага 8 (сгенерировать ключи, дописать [Peer], применить syncconf) стоит пройти один раз руками — чтобы понимать, что происходит внутри. Дальше для каждого нового устройства или пользователя быстрее и безопаснее через sc.sh: он не даст S1-S4/H1-H4 разъехаться (см. шаг 5 — и на своей шкуре в этом гайде), сам находит свободный IP и сам применяет изменение без разрыва тоннеля.
Добавить:
Bash:
sudo AWG_ENDPOINT="ВАШ_IP:44847" ./sc.sh add-client имя-устройства
Находит следующий свободный IP в подсети, генерит новую пару ключей, дописывает в awg0.conf блок [Peer] с меткой
# client: имя-устройства (по ней же потом ищется для удаления), применяет syncconf — уже подключённых клиентов не дёргает — и складывает готовый клиентский .conf в /etc/amnezia/amneziawg/clients/имя-устройства/. Имя уже занято — откажет, ничего не тронув.Удалить:
Bash:
sudo ./sc.sh remove-client имя-устройства
Убирает [Peer]-блок по метке, применяет syncconf (пир исчезает из рантайма немедленно, не только из файла на бумаге), стирает каталог с его ключами. Имени нет — откажет с понятным сообщением, файл не тронет.
Оба вызова — с одним sudo на всю команду, не вложенным (та же причина, что в шаге 8: awg/awg-quick внутри sc.sh уже не дублируют sudo сами).
Шаг 11. Полная мимикрия: I1-I5
Всё, что было до сих пор (H1-H4, S1-S4, Jc/Jmin/Jmax) — прячет отпечаток WireGuard, но не делает трафик похожим на что-то конкретное. Для DPI, который ищет сигнатуру WireGuard, — этого достаточно. Для DPI, который блокирует всё неопознанное независимо от сигнатуры, — не совсем: набор случайных байт без опознаваемой формы сам по себе может быть подозрителен.
I1-I5 — до пяти UDP-пакетов, отправляемых перед каждым хендшейком (а он повторяется раз в 120 секунд, не только при первом подключении), которые по заданному формату CPS могут выглядеть как настоящий QUIC/DNS/SIP или другой UDP-протокол. I1 обычно несёт байтовый слепок реального протокола, I2-I5 добавляют энтропию — счётчики, таймстемпы, случайные данные, — чтобы слепок не повторялся один в один при каждом хендшейке.
Что это реально даёт, а что нет. I1-I5 меняют то, как выглядит исходящий пакет для пассивного наблюдателя — не более того. Сервер на эти пакеты никак не отвечает и не умеет по-настоящему говорить на QUIC/DNS/SIP: если DPI не просто слушает, а активно зондирует порт, I1-I5 сам по себе такую проверку не пройдёт. Для ответа на активное зондирование нужна отдельная надстройка перед тоннелем — например, wiresock/amneziawg-proxy, который действительно отвечает на QUIC/DNS/STUN/SIP-пробы как настоящий сервис. Это отдельный инструмент, не то же самое, что I1-I5.
Официальный способ получить настоящий слепок — через Wireshark: ставишь фильтр на нужный протокол (quic, dns...), ловишь реальный пакет, копируешь его hex-байты в
<b 0xHEX>. Так и рекомендует сама Amnezia в документации.Важно написал:
Более быстрый путь без Wireshark — готовые общественные наборы (гуглится список VoidWaifu) или генератор конкретно под QUIC с нужным SNI. Это не праздный выбор: у отдельных хостеров (Hetzner — самый известный случай) в 2026 всплыла блокировка на уровне AS целиком — хендшейк проходит, а данные потом не идут. Помогает именно I1 с SNI, которому конкретно этот провайдер/эта сеть доверяет, а не универсальный пример; какой SNI сработает — заранее не угадать, дело подбора под конкретную сеть.
Синтаксис тегов:
<b 0xHEX>— буквальные байты, сюда идёт слепок реального протокола<r N>— N случайных байт<t>— unix-таймстемп<c>— счётчик<rc N>— N случайных букв (для текстовых протоколов вроде SIP)<rd N>— N случайных цифр
Можно комбинировать в одном параметре:
I1 = <b 0xc000000001><r 64><t>.Две вещи, которые ловятся молча, если забыть:
- Регистр обязателен — I1, не i1. Клиенты игнорируют строчные буквы без предупреждения.
- Включать/выключать надо симметрично. Значения I1-I5 могут различаться между клиентом и сервером (не то же самое, что S1-S4/H1-H4, где нужно точное совпадение) — но если хотя бы один параметр есть на одной стороне и полностью отсутствует на другой, хендшейк не сходится: стороны оказываются в двух разных режимах протокола, а не просто с разными данными.
Как включить через sc.sh:
Bash:
AWG_I1='<b 0xc000000001><r 64><t>' ./sc.sh init
Сервер уже проинициализирован без I1-I5 (см. шаг 5) — init не перезапишет существующий файл параметров, допиши I1=... в awg-obfuscation.env вручную той же строкой и перезапусти сервис. Для отдельного клиента с другим слепком, отличным от сохранённого в файле, передай переменную прямо перед вызовом block/add-client:
Bash:
AWG_I1='<b 0xAABBCCDD><r 32><t>' AWG_ENDPOINT="..." ./sc.sh add-client имя-с-другой-маской
Что дальше
Полноценная обфускация AWG 2.0 (заголовки, паддинг, мимикрия I1-I5) уже собрана — не хуже того, что накатывает приложение. Дальше по желанию:
- Пересобирать всё это руками каждый раз лень —
bivlked/amneziawg-installerиwiresock/amneziawg-installна GitHub автоматизируют весь путь (плюс QR, vpn://-ссылки, управление клиентами) — но тогда не будет понимания, что происходит внутри, если что-то сломается. - Если конечная цель не сервер, а своё приложение поверх протокола — движок amneziawg-go под MIT-лицензией специально сделан embeddable, ровно так собраны официальные Android/Windows-клиенты.
Ссылки на скачивание клиента:
Windows 10,11
MacOS 13
MacOS 12
MacOS 10,11
iOS и альтернатива
Android
Linux
GitHub
И краткая инструкция по автоматической настройке:
Берем нужную версию программы для своей системы по ссылке выше:
Далее устанавливаем и запускаем
нажимаем кнопку + и выбираем self-hosted
далее ввожу данные своего купленного сервера
где 127.0.0.1 мой ip адрес
22 это порт по умолчанию
root логин
и пароль
и нажимаю Continue продолжить
далее снова продолжаю так как меня интересует автоматическая установка, я ничего не трогаю
процесс пошел
Через 5 мин. будет готов личный vpn сервер
Далее после установки на главном экране увидим наш сервер с кнопкой Connect для подключения.
На этом все, получение личного впн завершено