Что нового

VPN без VPN: как сделать свой сервер на Ubuntu/Debian с нуля

Не люблю, когда сервер настраивает за меня чёрный ящик — пусть даже удобный. Приложение AmneziaVPN разворачивает AmneziaWG само: заходит по SSH, ставит модуль, генерит конфиги, рисует QR-код. Но что именно происходит на сервере — не видно, и в свою автоматизацию такое не воткнёшь. Поэтому поднимаю руками, шаг за шагом, с пониманием, что где происходит. Дальше сам решаешь: оставить как обычный systemd-сервис, обернуть в свой скрипт или утащить движок в собственное приложение.

Протокол — форк 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...

Если увидел эту ошибку написал:
Сборочному скрипту не хватило кусков в headers-пакете — по официальному README нужен полный сорцтри ядра, засимлинкованный как kernel внутри /usr/src/amneziawg-1.0.0/. Процедура для этого на Ubuntu/Debian не всегда гладкая от релиза к релизу (тема до сих пор живо обсуждается в issue-трекере проекта), и тащить через шаманство с apt-get source и подгонкой .config, которое может не завестись один-в-один под конкретную сборку ядра, — не мой путь. Честнее и надёжнее: на этой машине просто ставлю userspace-вариант (amneziawg-go, см. ниже) вместо модуля. Разницы в конфигах и командах awg/awg-quick — никакой, только сам движок другой.

Если 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 написал:
Если у провайдера VPS включён UEFI Secure Boot, самописный модуль без подписи ядро откажется грузить (modprobe вернёт "Key was rejected by service" или похожее). Тогда либо отключаю Secure Boot в панели провайдера, либо завожу MOK (Machine Owner Key) и подписываю модуль им — отдельная тема под конкретный дистрибутив, если упрёшься именно в это.

Альтернатива: 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 и файрвол провайдера написал:
Если ufw включён — он держит свои собственные FORWARD-цепочки и может резать транзитный трафик тоннеля даже при корректных iptables PostUp-правилах. Хендшейк есть, а трафик дальше сервера не идёт — проверяю sudo ufw route allow in on awg0 out on $SERVER_IFACE.

И отдельно проверяю файрвол/security group самого провайдера VPS — почти все облака держат ещё один firewall снаружи ОС, и это первое, что стоит проверить, если снаружи порт не отвечает при том что на сервере всё открыто.

Шаг 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 написал:
Оба sudo — одним вызовом, не двумя вложенными. Вариант sudo awg syncconf awg0 <(sudo awg-quick strip ...) (два отдельных sudo) падает с fopen: No such file or directory — bash создаёт файловый дескриптор под <(...) ещё до запуска внешней команды, а внешний sudo при своём собственном форке этот дескриптор не сохраняет, и к моменту открытия пути awg уже видит пустоту. Если сессия и так от root — 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 в документации.

Важно написал:
Не бери готовый пример из документации 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
1783535496476.png

далее ввожу данные своего купленного сервера
1783535748606.png


где 127.0.0.1 мой ip адрес
22 это порт по умолчанию
root логин
и пароль
и нажимаю Continue продолжить

далее снова продолжаю так как меня интересует автоматическая установка, я ничего не трогаю
1783535857034.png


процесс пошел
1783535935194.png


Через 5 мин. будет готов личный vpn сервер
Далее после установки на главном экране увидим наш сервер с кнопкой Connect для подключения.
На этом все, получение личного впн завершено
Об авторе
Guru
Василий, cистемный админ /gnu/linux/windows/macos/mikrotik/troubleshooter, создатель сайта
Интересуюсь всем что делает инфраструктуру быстрой и надёжной
Открыт к общению и проектам, написать мне можно через форму или в личном сообщении

❗ Если есть пожелания по обзору какого-либо вопроса не представленного на сайте - пиши в комментариях

Комментарии

Нет комментариев для отображения.

Информация о статье

Автор
Guru
Время чтения статьи
18 мин чтения
Просмотры
19
Посл. обновление

Ещё от Guru

Поделиться этой статьёй

Назад
Верх