Вступление
В этом мануале собирается связка:
Итоговая схема будет такой:
Что получится:
Unbound — это validating, recursive, caching DNS resolver, то есть он может сам рекурсивно резолвить домены и кэшировать ответы, а не просто пересылать всё в Cloudflare или Google.
Как будет работать
Пользователь подключается к WireGuard-клиенту, получает VPN-адрес, например:
В его WireGuard-конфиге будет:
Это значит:
Даже если пользователь вручную поменяет DNS на 1.1.1.1 или 8.8.8.8, обычный DNS на 53/tcp и 53/udp будет принудительно перенаправляться в AdGuard Home.
Итоговые адреса и порты
Наружу открывается только:
Наружу не открываем:
Важно
В этой версии мануала IPv6 не используется.
DISABLE_IPV6=true0.0.0.0/0::/0В WG-Easy v15 есть переменная DISABLE_IPV6=true. Она отключает IPv6-поддержку у WG-Easy, но IPv6 CIDR всё равно может отображаться в Web UI как поле формы.
Поэтому IPv6 CIDR в интерфейсе не нужно удалять — оставь валидное значение вроде fd42:42:42::/64.
1. Установка Docker на Ubuntu 26.04
В этом мануале Docker устанавливается через официальный APT-репозиторий Docker, а не через быстрый скрипт:
curl -fsSL https://get.docker.com | sudo sh
Для чистой установки Ubuntu 26.04 лучше использовать официальный репозиторий Docker. Так Docker Engine и Docker Compose Plugin будут обновляться штатно через apt.
Шаг 1. Удалить конфликтующие старые пакеты
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt remove -y "$pkg" 2>/dev/null || true
done
Эта команда удаляет старые или конфликтующие пакеты Docker, если они были установлены из стандартного репозитория Ubuntu.
Шаг 2. Установить зависимости
sudo apt update
sudo apt install ca-certificates curl -y
Что делает команда:
Шаг 3. Добавить официальный GPG-ключ Docker
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Проверить:
ls -l /etc/apt/keyrings/docker.asc
Ожидаемо должен быть файл:
Шаг 4. Проверить codename Ubuntu
. /etc/os-release && echo "$VERSION_CODENAME"
Для Ubuntu 26.04 должно быть:
Проверить архитектуру:
dpkg --print-architecture
Обычно на VPS будет:
Шаг 5. Создать Docker APT source
Открыть файл:
sudo nano /etc/apt/sources.list.d/docker.sources
Вставить:
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: resolute
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
Если VPS использует не amd64, замени строку:
на свою архитектуру из команды:
dpkg --print-architecture
Шаг 6. Установить Docker Engine и Docker Compose Plugin
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
Что проверить после шага
docker --version
docker compose version
sudo systemctl status docker --no-pager
sudo docker run hello-world
Ожидаемо:
Если hello-world запустился без ошибок, Docker готов к установке WG-Easy, AdGuard Home и Unbound.
2. Создать папки проекта
sudo mkdir -p /opt/wg-easy
sudo mkdir -p /opt/wg-easy/adguard/work
sudo mkdir -p /opt/wg-easy/adguard/conf
sudo mkdir -p /opt/wg-easy/unbound/data
cd /opt/wg-easy
Права для AdGuard:
sudo chmod -R 700 /opt/wg-easy/adguard/work
sudo chmod -R 700 /opt/wg-easy/adguard/conf
AdGuard Home Docker-образ использует две постоянные директории: одну для рабочих данных и одну для конфигурации.
3. Создать Dockerfile для Unbound
Откройте файл:
sudo nano /opt/wg-easy/unbound/Dockerfile
Вставьте:
FROM alpine:3.21
RUN apk add --no-cache unbound ca-certificates wget bind-tools
COPY unbound.conf /etc/unbound/unbound.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 5335/tcp 5335/udp
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD dig @127.0.0.1 -p 5335 example.com A +short >/dev/null || exit 1
ENTRYPOINT ["/entrypoint.sh"]
4. Создать entrypoint для Unbound
Откройте файл:
sudo nano /opt/wg-easy/unbound/entrypoint.sh
Вставьте:
#!/bin/sh
set -e
mkdir -p /var/lib/unbound
if [ ! -f /var/lib/unbound/root.hints ]; then
wget -q -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
fi
unbound-anchor -a /var/lib/unbound/root.key || true
chown -R unbound:unbound /var/lib/unbound
exec unbound -d -c /etc/unbound/unbound.conf
5. Создать конфиг Unbound
Откройте файл:
sudo nano /opt/wg-easy/unbound/unbound.conf
Вставьте:
server:
verbosity: 1
interface: 0.0.0.0
port: 5335
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
access-control: 127.0.0.0/8 allow
access-control: 10.42.42.0/24 allow
root-hints: "/var/lib/unbound/root.hints"
auto-trust-anchor-file: "/var/lib/unbound/root.key"
username: "unbound"
directory: "/var/lib/unbound"
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
harden-algo-downgrade: yes
use-caps-for-id: no
qname-minimisation: yes
minimal-responses: yes
edns-buffer-size: 1232
prefetch: yes
prefetch-key: yes
msg-cache-size: 64m
rrset-cache-size: 128m
cache-min-ttl: 0
cache-max-ttl: 86400
val-clean-additional: yes
Что делает этот конфиг
6. Создать финальный docker-compose.yml
Откройте файл:
sudo nano /opt/wg-easy/docker-compose.yml
Вставьте:
WG-Easy официально использует Docker Compose как основной пример запуска, а для v15 рекомендуется использовать тег ghcr.io/wg-easy/wg-easy:15, а не полагаться на старый latest.
Важно
В compose-файле специально нет:
- "53:53/tcp"- "53:53/udp"Это правильно.
AdGuard Home слушает 53 только внутри Docker-сети:
10.42.42.43:53На сам VPS порт 53 не пробрасывается. Поэтому если на VPS порт 53 занят systemd-resolved, это не мешает.
7. Запуск стека
Перед первым запуском обязательно проверим docker-compose.yml на ошибки.
Открыть папку проекта:
cd /opt/wg-easy
Проверить итоговую конфигурацию Docker Compose:
sudo docker compose config
Если файл написан правильно, команда выведет итоговую объединённую конфигурацию без ошибок.
Если в docker-compose.ym есть ошибка в отступах, двоеточиях или структуре YAML, Docker Compose покажет ошибку ещё до запуска контейнеров.
Типичные ошибки YAML:
Если ошибок нет, запускаем стек:
sudo docker compose up -d --build
Проверить контейнеры:
sudo docker compose ps
Ожидаемо:
Посмотреть логи:
sudo docker logs wg-easy --tail=100
sudo docker logs adguard-vpn-dns --tail=100
sudo docker logs unbound-vpn-dns --tail=100
Что проверить после шага
Проверить, какие порты слушаются на VPS:
sudo ss -luntp | grep -E ':51820|:51821|:3000|:53'
Нормально:
Проверить Docker-пробросы:
sudo docker port adguard-vpn-dns
sudo docker port wg-easy
Для AdGuard Home ожидаемо:
У AdGuard не должно быть:
Если порт 53 проброшен наружу, это ошибка. В этой схеме AdGuard DNS должен быть доступен только внутри Docker-сети как:
8. Открыть firewall
Если используете UFW:
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw enable
sudo ufw status
Не открывайте:
9. Открыть WG-Easy через SSH-туннель
На Windows PowerShell:
ssh -N -L 51821:127.0.0.1:51821 root@IP_ВАШЕГО_VPS
Окно PowerShell не закрывайте.
В браузере:
http://127.0.0.1:51821
10. Первичная настройка WG-Easy
В WG-Easy укажите:
Замените vpn.example.com на свой домен или поддомен. Например: secure.jeyber.com.
Да, IPv6 CIDR оставляем заполненным, но IPv6 фактически отключаем через:
Ошибка новичка
Не удаляйте IPv6 CIDR в WG-Easy UI.
Неправильно:
пустоПравильно:
fd42:42:42::/64DISABLE_IPV6=trueЕсли удалить IPv6 CIDR, WG-Easy может ругаться на невалидное значение. При DISABLE_IPV6=true этот CIDR не должен попадать в клиентские конфиги.
11. Открыть AdGuard Home через SSH-туннель
AdGuard Home Web UI в этой схеме не открыт наружу. Он доступен только локально на VPS:
С Windows PowerShell открыть SSH-туннель:
ssh -N -L 3000:127.0.0.1:3000 root@IP_ВАШЕГО_VPS
Окно PowerShell не закрывать. Оно держит туннель.
После этого открыть в браузере на Windows:
http://127.0.0.1:3000
Если порт 3000 уже занят на Windows, можно использовать локальный порт 3001:
ssh -N -L 3001:127.0.0.1:3000 root@IP_ВАШЕГО_VPS
Тогда в браузере открыть:
http://127.0.0.1:3001
Если AdGuard Home не открывается
Сначала проверить на самом VPS:
curl -I http://127.0.0.1:3000
Нормально, если ответ будет похож на один из этих вариантов:
или:
или:
Проверить, слушается ли порт 3000 на VPS:
sudo ss -luntp | grep ':3000'
Правильный результат выглядит примерно так:
Это значит, что AdGuard Home доступен только локально на VPS. Это правильно.
Проверить Docker-проброс:
sudo docker port adguard-vpn-dns
Ожидаемо:
Если curl на VPS отвечает, а в браузере на Windows http://127.0.0.1:3000 не открывается, проблема почти всегда в SSH-туннеле.
Правильная команда для Windows PowerShell:
ssh -N -L 3000:127.0.0.1:3000 root@IP_ВАШЕГО_VPS
Если AdGuard был случайно настроен на порт 80, а не 3000, проверить конфиг:
sudo nano /opt/wg-easy/adguard/conf/AdGuardHome.yaml
Найти блок:
http:address: 0.0.0.0:80Заменить на:
http:address: 0.0.0.0:3000Перезапустить AdGuard:
cd /opt/wg-easy
sudo docker compose restart adguard
Проверить снова:
curl -I http://127.0.0.1:3000
12. Первичная настройка AdGuard Home
При первом открытии AdGuard Home появится мастер настройки.
В мастере указать:
Это безопасно, потому что в docker-compose.yml порт 53 не проброшен наружу на VPS. AdGuard будет слушать 53 только внутри Docker-сети.
После установки открыть:
Указать upstream DNS:
Это значит:
В Bootstrap DNS servers можно оставить:
Для upstream 10.42.42.44:5335 bootstrap почти не нужен, потому что upstream задан IP-адресом.
Что проверить после шага
Проверить Unbound напрямую:
sudo docker run --rm --network wg alpine:3.21 sh -lc 'apk add --no-cache bind-tools >/dev/null && dig @10.42.42.44 -p 5335 example.com A +short'
Ожидаемо должны вернуться IP-адреса.
Проверить AdGuard → Unbound:
sudo docker run --rm --network wg alpine:3.21 sh -lc 'apk add --no-cache bind-tools >/dev/null && dig @10.42.42.43 example.com A +short'
Если есть ответ, цепочка работает:
10.42.42.43:53 → Unbound 10.42.42.44:5335Важно про DoH
Принудительный DNS через AdGuard перехватывает обычный DNS на:
Также в hooks можно заблокировать:
Но DNS-over-HTTPS полностью не отличить от обычного HTTPS только iptables-правилами, потому что он идёт через:
Для своих устройств лучше отключить Secure DNS в браузере.
Chrome / Edge:
Firefox:
Опционально: блокировка QUIC / HTTP3 через UDP/443
Некоторые браузеры и приложения используют QUIC/HTTP3 через:
Обычные сайты при этом используют HTTPS через:
Если заблокировать 443/udp, большинство сайтов продолжит открываться через обычный HTTPS 443/tcp, но QUIC/HTTP3 будет отключён.
Это может быть полезно, если нужно сделать более строгий режим и уменьшить количество обходных вариантов.
Добавить блокировку UDP/443
В WG-Easy открыть:
В PostUp можно добавить к существующим правилам:
iptables -A FORWARD -i wg0 -p udp --dport 443 -j REJECT;
В PostDown тогда обязательно добавить:
iptables -D FORWARD -i wg0 -p udp --dport 443 -j REJECT || true;
После сохранения перезапустить WG-Easy:
sudo docker restart wg-easy
Проверить, что правило появилось:
sudo docker exec -it wg-easy sh -lc 'iptables -S FORWARD | grep "dport 443"'
Ожидаемо:
Важно
Сначала лучше проверить базовую схему без блокировки 443/udp.
Сначала настраиваем:
Потом проверяем:
И только после этого, при необходимости, добавляем блокировку 443/udp.
Блокировка 443/udp может влиять на приложения, которые активно используют QUIC/HTTP3. Обычно они переключаются на 443/tcp, но проверять это нужно отдельно.
13. Проверить Unbound
На VPS:
sudo docker run --rm --network wg alpine:3.21 sh -lc 'apk add --no-cache bind-tools >/dev/null && dig @10.42.42.44 -p 5335 example.com A +short'
Нормально, если вернутся IP-адреса:
IP могут отличаться. Главное — чтобы был ответ.
14. Проверить AdGuard → Unbound
sudo docker run --rm --network wg alpine:3.21 sh -lc 'apk add --no-cache bind-tools >/dev/null && dig @10.42.42.43 example.com A +short'
Если есть ответ, цепочка работает:
15. Настроить принудительный DNS в WG-Easy Hooks
Это нужно, чтобы пользователь не мог просто поменять DNS на 1.1.1.1, 8.8.8.8 или другой обычный DNS-сервер.
В WG-Easy откройте:
WG-Easy поддерживает server-level hooks, которые выполняются при старте и остановке WireGuard-интерфейса.
PostUp
Вставьте в PostUp:
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43:53; iptables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43:53; iptables -A FORWARD -i wg0 -p tcp --dport 853 -j REJECT; iptables -A FORWARD -i wg0 -p udp --dport 853 -j REJECT; iptables -A FORWARD -i wg0 -p udp --dport 784 -j REJECT; iptables -A FORWARD -i wg0 -p udp --dport 8853 -j REJECT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE;
PostDown
Вставьте в PostDown:
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; iptables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43:53 || true; iptables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43:53 || true; iptables -D FORWARD -i wg0 -p tcp --dport 853 -j REJECT || true; iptables -D FORWARD -i wg0 -p udp --dport 853 -j REJECT || true; iptables -D FORWARD -i wg0 -p udp --dport 784 -j REJECT || true; iptables -D FORWARD -i wg0 -p udp --dport 8853 -j REJECT || true; iptables -D FORWARD -i wg0 -j ACCEPT || true; iptables -D FORWARD -o wg0 -j ACCEPT || true; iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE || true;
Официальный пример WG-Easy с AdGuard Home тоже использует hooks и DNAT DNS-запросов на AdGuard Home; здесь оставлена только IPv4-логика и добавлена блокировка DoT/DoQ-портов.
После сохранения:
sudo docker restart wg-easy
Что делают эти hooks
Обычный DNS перехватывается:
Если пользователь вручную поставит DNS:
обычный DNS всё равно попадёт в AdGuard Home.
Дополнительно блокируются:
Важно про DoH
DNS-over-HTTPS полностью не отличить от обычного HTTPS только iptables-правилами, потому что он идёт через:
Для своих устройств отключайте Secure DNS в браузерах:
Опционально можно заблокировать 443/udp, чтобы отключить QUIC/HTTP3:
16. Проверить, что hooks применились
После перезапуска WG-Easy:
sudo docker exec -it wg-easy sh -lc 'iptables -t nat -S | grep 10.42.42.43'
Должно быть что-то вроде:
Проверить FORWARD:
sudo docker exec -it wg-easy sh -lc 'iptables -S FORWARD'
Проверить NAT:
sudo docker exec -it wg-easy sh -lc 'iptables -t nat -S POSTROUTING'
17. Создать нового клиента в WG-Easy
В WG-Easy:
Например:
Скачайте .conf.
Если старый ключ когда-либо публиковался или отправлялся в чат, лучше удалить старого клиента и создать нового.
18. Каким должен быть клиентский WireGuard-конфиг
Правильный вид:
Если WG-Easy создаёт:
можно оставить, но для клиентов за NAT, роутером или мобильной сетью лучше:
Без IPv6 в клиентском конфиге должно быть так
Должно быть:
Не должно быть:
Должно быть:
Не должно быть:
19. Kill switch на Windows и проверка IPv6
После импорта конфига в WireGuard for Windows:
Так как в этом мануале используется IPv4-only схема, kill switch важен. Он помогает избежать ситуации, когда часть трафика или IPv6 уходит мимо VPN.
Проверка IPv4 после подключения
После подключения WireGuard открыть Windows PowerShell и проверить внешний IPv4:
curl.exe -4 https://ifconfig.me
Должен отобразиться IPv4-адрес VPS.
Например:
Если отображается домашний IP провайдера, значит трафик не пошёл через VPN.
Проверка IPv6 после подключения
Так как в этом мануале используется IPv4-only схема, IPv6 через VPN не выдаётся.
Проверить IPv6:
curl.exe -6 https://ifconfig.me
Для этой схемы нормально, если команда не вернёт IPv6-адрес или завершится ошибкой подключения.
Если команда показывает IPv6 домашнего провайдера, значит IPv6 уходит мимо VPN.
В этом случае обязательно:
Если IPv6 всё равно виден, можно временно отключить IPv6 на сетевом адаптере Windows.
Проверка через сайт
Открыть в браузере:
https://test-ipv6.com
Для этой схемы ожидаемо:
Если сайт показывает IPv6 домашнего провайдера, это утечка IPv6.
Проверка DNS
Проверить обычный DNS:
nslookup example.com
DNS-сервер должен быть:
Это AdGuard Home.
Проверить принудительный DNS:
nslookup example.com 1.1.1.1
И:
nslookup example.com 8.8.8.8
После этих команд открыть:
Если правила DNAT работают, запросы должны появиться в журнале AdGuard Home, даже если в команде указан 1.1.1.1 или 8.8.8.8.
Что должно быть в клиентском конфиге без IPv6
В клиентском .conf должно быть:
Не должно быть:
Должно быть:
Не должно быть:
DNS должен быть:
Итог проверки
Рабочее состояние выглядит так:
20. Финальная проверка на VPS
Проверить контейнеры:
cd /opt/wg-easy
sudo docker compose ps
Ожидаемо:
Проверить Unbound:
sudo docker run --rm --network wg alpine:3.21 sh -lc 'apk add --no-cache bind-tools >/dev/null && dig @10.42.42.44 -p 5335 example.com A +short'
Проверить AdGuard:
sudo docker run --rm --network wg alpine:3.21 sh -lc 'apk add --no-cache bind-tools >/dev/null && dig @10.42.42.43 example.com A +short'
Проверить IPv6 в WG-Easy:
sudo docker inspect wg-easy --format '{{range .Config.Env}}{{println .}}{{end}}' | grep DISABLE_IPV6
Должно быть:
Проверить wg0 внутри контейнера:
sudo docker exec -it wg-easy sh -lc 'ip addr show wg0'
Не должно быть адреса:
21. Финальная проверка на Windows
После подключения WireGuard:
curl.exe https://ifconfig.me
Должен быть IP твоего VPS.
DNS:
nslookup example.com
Проверка принудительного DNS:
nslookup example.com 1.1.1.1
nslookup example.com 8.8.8.8
После этих команд открой:
Запросы должны появиться в AdGuard. Это значит, что даже при попытке использовать 1.1.1.1 или 8.8.8.8 обычный DNS перенаправляется в AdGuard.
Проверка IPv6:
https://test-ipv6.com
В этой схеме IPv6 через VPN не используется. Если сайт показывает внешний IPv6 провайдера клиента, значит на клиенте нужно включить kill switch или отключить IPv6 на сетевом адаптере.
22. Как будет в итоге
Итоговая работа:
Частые ошибки
Ошибка 1. Открыть AdGuard DNS наружу
Не надо:
Правильно: 53 только внутри Docker-сети.
Ошибка 2. Указать Unbound в WG-Easy DNS
Неправильно:
Правильно:
Unbound указывается только в AdGuard:
Ошибка 3. Удалить IPv6 CIDR
Неправильно:
Правильно:
Ошибка 4. Открывать WG-Easy и AdGuard напрямую
Неправильно:
Правильно:
И открывать:
Ошибка 5. Не пересоздать клиента после изменения DNS
Если клиент был создан до настройки DNS, в его .conf может быть старый DNS.
Правильно:
В новом конфиге должно быть: