Свой VPN на WireGuard: сервер на Ubuntu, клиенты на Windows и Android

Четыре года назад я писал, как поднять свой IKEv2 VPN на strongSwan, и эта статья до сих пор одна из самых читаемых в блоге. С тех пор у IKEv2 появился конкурент, который делает то же самое заметно проще: WireGuard. Там, где strongSwan требует удостоверяющий центр, сертификаты, выбор алгоритмов и три конфигурационных файла, WireGuard обходится парой ключей и десятью строками. Сегодня — как поднять такой сервер на Ubuntu с нуля и подключить к нему Windows и Android.

Что такое VPN в терминах интерфейсов и маршрутов, чем полный туннель отличается от раздельного и откуда берутся утечки DNS и IPv6, разобрано в двадцатой части «Сетей для самых маленьких». Здесь это не повторяется, зато всё, что встречается по ходу, объясняется на месте. Как устроен протокол WireGuard внутри — рукопожатие, криптография, формат пакетов — для настройки знать не нужно, и в этой статье этого нет.

Адреса в статье документационные: сервер 203.0.113.10 и 2001:db8::10, сеть туннеля 10.66.66.0/24. Ключи — заглушки в угловых скобках.


Как устроен WireGuard

Пять вещей, которых достаточно, чтобы понимать каждую строку конфига.

Ключи вместо сертификатов и паролей. У каждого участника — сервера, ноутбука, телефона — своя пара ключей: закрытый, который никуда не уходит, и открытый, который сообщают другой стороне. Сервер знает открытые ключи всех своих клиентов, клиент — открытый ключ сервера. Этого достаточно и чтобы узнать друга, и чтобы договориться о шифровании. Ни удостоверяющего центра, ни сроков действия, ни логинов. Открытый ключ — это и есть «имя» устройства в WireGuard. Ключ — 32 байта, записанные в base64, строка из 44 символов вида gI6EdUSYvn8ugXOt8QQD6Yc1SMvDv31U6PSYwpeIE1E=.

Один UDP-порт. WireGuard работает поверх UDP (девятая часть), по умолчанию порт 51820, но можно любой. Нет отдельных портов для согласования и для данных, как у IPsec, нет TCP-соединения, которое нужно держать. Клиент может сменить адрес — переключиться с Wi-Fi на мобильную сеть, — и туннель продолжит работать с нового адреса без переподключения: сервер просто запомнит, откуда пришёл последний правильный пакет.

Интерфейс и маршруты. Как и любой VPN, WireGuard на машине — это сетевой интерфейс (wg0) с адресом из «внутренней» сети туннеля и маршруты, которые в него ведут. Пакет, попавший в интерфейс, шифруется и уезжает по UDP на адрес другой стороны; пришедший по UDP — расшифровывается и появляется из интерфейса как обычный IP-пакет. Всё остальное — дело таблицы маршрутов.

AllowedIPs — маршруты и список доступа сразу. Это главная настройка, и она делает две вещи одновременно. Для исходящих пакетов: «пакеты к этим адресам отправлять этому пиру», то есть маршрут. Для входящих: «от этого пира принимать пакеты только с этих адресов отправителя», то есть фильтр. У клиента AllowedIPs = 0.0.0.0/0, ::/0 значит «всё — в туннель» (полный туннель) и «от сервера принимаю ответы откуда угодно». У сервера для каждого клиента AllowedIPs = 10.66.66.2/32 значит «ему — только пакеты для его адреса» и «от него — только пакеты с его адреса»: клиент не сможет подставить чужой адрес, даже если захочет. Эту конструкцию называют cryptokey routing: маршрутизация по ключам.

Нет понятия «подключён». WireGuard не устанавливает соединение. Когда есть что отправить, он проверяет, есть ли свежие ключи сеанса с этим пиром (они живут две минуты), и если нет — делает рукопожатие: один пакет туда, один обратно, и дальше идут данные. Когда данных нет, нет и пакетов, вообще никаких. Из-за этого статус «подключено» в клиентах условный, а чтобы домашний роутер не забыл про UDP-поток за время тишины, есть PersistentKeepalive — о нём ниже.

Чем это отличается от IKEv2 на практике:

IKEv2/IPsec WireGuard
Что нужно, чтобы доверять друг другу сертификаты и удостоверяющий центр, или имя с паролем через EAP открытые ключи обеих сторон
Выбор криптографии согласуется при подключении из списка; можно настроить слабо фиксирован, настраивать нечего
Порты UDP 500 и 4500, протокол ESP один UDP-порт
Смена адреса клиента MOBIKE, если обе стороны умеют само собой
Встроен в ОС Windows, macOS, iOS, Android — без программ нужен клиент; в ядро Linux встроен с версии 5.6
Конфиг сервера несколько файлов и секций один файл, десять строк на сервер и четыре на клиента
Код сотни тысяч строк около четырёх тысяч

Встроенность IKEv2 в операционные системы — его главный плюс, и по-прежнему причина, по которой он живёт в корпоративных сетях. Для своего сервера, к которому подключаются свои же устройства, разница не важна: клиент WireGuard ставится за минуту.


Сервер

Нужна виртуальная машина с публичным адресом и Ubuntu. Я беру Ubuntu 26.04, но всё ниже одинаково работает с 20.04: модуль WireGuard есть в ядре начиная с 5.6, а в более старые ядра Ubuntu его бэкпортировала. Ресурсов хватает самых маленьких — 1 vCPU и 1 ГБ памяти держат десяток клиентов. Вход по SSH ключом под root.

Установка

apt update && apt install -y wireguard-tools qrencode
wg --version

Пакет wireguard-tools — утилиты wg (управление интерфейсом) и wg-quick (поднимает интерфейс по конфигу: адреса, маршруты, DNS). Сам WireGuard — модуль ядра, ставить его не нужно; на старых ядрах вместо wireguard-tools ставится метапакет wireguard, который подтянет модуль. qrencode понадобится для телефона.

Ключи сервера

umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
cat /etc/wireguard/server.pub      # открытый ключ — его получат клиенты

umask 077 — чтобы файлы создались с правами только для владельца: закрытый ключ сервера — самый важный секрет здесь, с ним можно выдать себя за ваш сервер. wg genkey печатает закрытый ключ, wg pubkey читает закрытый и печатает открытый.

Конфиг сервера

/etc/wireguard/wg0.conf:

[Interface]
Address = 10.66.66.1/24, fd66:6666:6666::1/64
ListenPort = 51820
PrivateKey = <закрытый ключ сервера>

Это ядро конфига; правила файрвола добавятся в него ниже. Address — адреса интерфейса wg0 в сети туннеля, IPv4 и IPv6; клиенты получат адреса из этих же сетей. ListenPort — UDP-порт. PrivateKey — содержимое server.key; сам файл после этого можно удалить, ключ живёт в конфиге. Клиенты будут добавляться ниже секциями [Peer].

Сеть туннеля — любая частная, которая не пересекается с сетями, где бывают клиенты: 192.168.0.0/24 и 192.168.1.0/24 стоят в каждом втором роутере, поэтому 10.66.66.0/24 безопаснее. IPv6-сеть — ULA, unique local address, из диапазона fd00::/8: аналог частных адресов для IPv6, в интернет не маршрутизируется.

Форвардинг и NAT

Пока сервер умеет только принимать пакеты из туннеля. Чтобы передавать их дальше в интернет и возвращать ответы, нужны две вещи.

Форвардинг — разрешение ядру пересылать пакеты между интерфейсами, по умолчанию выключенное:

cat > /etc/sysctl.d/99-wireguard.conf <<EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl --system

NAT — подмена адреса отправителя. Пакет клиента уходит в интернет с адреса 10.66.66.2, на который никто не сможет ответить: это частный адрес. Сервер должен заменить его на свой публичный адрес, а ответ вернуть клиенту — ровно то, что делает домашний роутер (шестая часть). В Linux это правило MASQUERADE в таблице nat netfilter — подсистемы фильтрации пакетов в ядре, которой управляют командой iptables (и ip6tables для IPv6).

Правила удобно держать прямо в конфиге интерфейса: у wg-quick есть PostUp — команды, которые выполняются после поднятия интерфейса, и PostDown — после его остановки. Правила появляются вместе с туннелем и исчезают вместе с ним, ничего сохранять и восстанавливать при загрузке не нужно. Конфиг сервера целиком:

[Interface]
Address = 10.66.66.1/24, fd66:6666:6666::1/64
ListenPort = 51820
PrivateKey = <закрытый ключ сервера>

# IPv4: транзит из туннеля в интернет, ответы обратно, всё остальное из туннеля — запретить; NAT
PostUp   = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
PostUp   = iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp   = iptables -A FORWARD -i wg0 -j DROP
PostUp   = iptables -t nat -A POSTROUTING -s 10.66.66.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT
PostDown = iptables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j DROP
PostDown = iptables -t nat -D POSTROUTING -s 10.66.66.0/24 -o eth0 -j MASQUERADE

# IPv6: то же самое
PostUp   = ip6tables -A FORWARD -i wg0 -o eth0 -j ACCEPT
PostUp   = ip6tables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp   = ip6tables -A FORWARD -i wg0 -j DROP
PostUp   = ip6tables -t nat -A POSTROUTING -s fd66:6666:6666::/64 -o eth0 -j MASQUERADE
PostDown = ip6tables -D FORWARD -i wg0 -o eth0 -j ACCEPT
PostDown = ip6tables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = ip6tables -D FORWARD -i wg0 -j DROP
PostDown = ip6tables -t nat -D POSTROUTING -s fd66:6666:6666::/64 -o eth0 -j MASQUERADE

eth0 — интерфейс сервера, смотрящий в интернет; проверьте своё имя командой ip -br addr. Что делают правила, по порядку:

  • FORWARD -i wg0 -o eth0 -j ACCEPT — пакеты, пришедшие из туннеля и уходящие в интернет, пропускать. Цепочка FORWARD в iptables — это транзит: пакеты, которые сервер не принимает себе и не отправляет сам, а пересылает между интерфейсами.
  • FORWARD -i eth0 -o wg0 … RELATED,ESTABLISHED -j ACCEPT — обратно пропускать только ответы на соединения, начатые из туннеля. conntrack — таблица соединений в ядре: по ней netfilter помнит, кто что начал, и отличает ответ от попытки снаружи постучаться к клиенту VPN.
  • FORWARD -i wg0 -j DROP — всё остальное из туннеля отбросить. Это защита других сетей сервера: если у машины есть второй интерфейс в частную сеть хостинга или туннели куда-то ещё, клиенты VPN туда не попадут. Порядок важен: правила проверяются сверху вниз, и DROP стоит после разрешений. Без этого правила при политике FORWARD ACCEPT (а она по умолчанию такая) клиент мог бы ходить через сервер куда угодно.
  • nat POSTROUTING -s 10.66.66.0/24 -o eth0 -j MASQUERADE — пакетам из сети туннеля, уходящим в eth0, подставить адрес eth0. POSTROUTING — точка после решения о маршруте, когда уже известно, через какой интерфейс пакет уйдёт.

PostDown повторяет те же команды с -D вместо -A: удалить правило. Если туннель перезапускается, правила не накапливаются.

Про IPv6 отдельно. NAT для IPv6 — вещь непривычная: адресов там хватает всем, и правильно было бы раздать клиентам настоящие публичные адреса из блока сервера. Но для этого хостинг должен маршрутизировать на сервер целый блок, обычно /64, а многие дают серверу один адрес или несколько штук. Тогда остаётся NAT66: клиенты выходят с IPv6-адреса сервера. Это лучше, чем не выдавать клиентам IPv6 вовсе: клиент, у которого дома есть IPv6, а в туннеле нет, отправит трафик к сайтам с AAAA-записями мимо туннеля, в открытую. Если ваш хостинг маршрутизирует /64, выдайте клиентам адреса из него и NAT66 не делайте — тогда и правила POSTROUTING для ip6tables не нужны, остаются только транзитные.

Входящий порт. На чистой Ubuntu входящие пакеты не фильтруются, и порт 51820 открыт сам по себе. Если на сервере файрвол с политикой INPUT DROP, добавьте разрешение — например, туда же в PostUp:

PostUp   = iptables -A INPUT -p udp --dport 51820 -j ACCEPT
PostUp   = ip6tables -A INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT
PostDown = ip6tables -D INPUT -p udp --dport 51820 -j ACCEPT

У облачных провайдеров файрвол часто есть ещё и снаружи машины, в панели управления: UDP 51820 нужно открыть и там. Если файрволом на сервере управляет ufw, правила из PostUp с ним не конфликтуют, но порт открывается его командой ufw allow 51820/udp, а транзит и NAT аккуратнее перенести в его файлы /etc/ufw/before.rules и before6.rules: ufw при перезагрузке правил сбрасывает цепочки и свои правила восстанавливает, а чужие — нет.

Запуск

chmod 600 /etc/wireguard/wg0.conf
systemctl enable --now wg-quick@wg0
wg show wg0

wg-quick@wg0 — шаблонная служба systemd: wg0 в имени — это имя конфига в /etc/wireguard/. Она создаёт интерфейс, назначает адреса, ставит маршрут в сеть туннеля, загружает ключи и выполняет PostUp. wg show покажет интерфейс, его открытый ключ и порт; пиров пока нет. Правила файрвола должны быть на месте:

iptables -S FORWARD
iptables -t nat -S POSTROUTING

В первом выводе — три правила с wg0, во втором — MASQUERADE.


Клиенты: добавление и отзыв

У каждого устройства — свои ключи. Тогда потерянный телефон отзывается одной командой, а остальные устройства ничего не замечают. Для каждого устройства нужно: сгенерировать пару ключей, выбрать свободный адрес в сети туннеля, добавить секцию [Peer] на сервер и собрать конфиг клиента. Руками это пять минут и две ошибки на устройство, поэтому — скрипт. Положите его в /usr/local/sbin/wg-user и сделайте исполняемым (chmod 755).

#!/usr/bin/env bash
# wg-user list | add <name> | show <name> | del <name>
set -euo pipefail

WG_IF=wg0
CFG=/etc/wireguard/$WG_IF.conf
CLIENTS=/etc/wireguard/clients
NET4=10.66.66                 # клиенты получают $NET4.N/32
NET6=fd66:6666:6666::         # и ${NET6}N/128
ENDPOINT=203.0.113.10:51820
DNS="1.1.1.1, 2606:4700:4700::1111"
KEEPALIVE=25

die() { echo "wg-user: $*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "run as root"
umask 077
mkdir -p "$CLIENTS"

check_name() { [[ "$1" =~ ^[A-Za-z0-9._@-]{1,64}$ ]] || die "bad name"; }
has_user() { grep -qx "# user: $1" "$CFG"; }
peer_pubkey() { awk -v n="# user: $1" '$0==n{f=1} f&&/^PublicKey/{print $3; exit}' "$CFG"; }
server_pubkey() { awk '/^PrivateKey/{print $3; exit}' "$CFG" | wg pubkey; }
sync() { wg syncconf "$WG_IF" <(wg-quick strip "$WG_IF"); }

next_host() {
    local used n
    used=$(grep -oE "AllowedIPs = $NET4\.[0-9]+" "$CFG" | awk -F. '{print $NF}' | sort -n)
    for n in $(seq 2 254); do grep -qx "$n" <<<"$used" || { echo "$n"; return; }; done
    die "no free addresses"
}

show_conf() {
    local f=$CLIENTS/$1.conf
    [ -f "$f" ] || die "no client config for $1"
    cat "$f"; echo
    qrencode -t ansiutf8 < "$f"
}

case "${1:-}" in
    list)
        declare -A names
        while read -r name pub; do names[$pub]=$name; done < <(awk '/^# user: /{n=$3} /^PublicKey/{print n, $3}' "$CFG")
        printf '%-20s %-34s %-24s %s\n' NAME ADDRESSES ENDPOINT LAST_HANDSHAKE
        wg show "$WG_IF" dump | tail -n +2 | while IFS=$'\t' read -r pub psk endpoint ips hs rx tx ka; do
            if [ "$hs" = 0 ]; then hs_s=never; else hs_s="$(( $(date +%s) - hs ))s ago"; fi
            printf '%-20s %-34s %-24s %s\n' "${names[$pub]:-?}" "${ips//,/ }" "${endpoint:-(none)}" "$hs_s"
        done ;;
    add)
        [ $# -eq 2 ] || die "usage: wg-user add <name>"; check_name "$2"
        has_user "$2" && die "user $2 already exists"
        n=$(next_host); priv=$(wg genkey); pub=$(wg pubkey <<<"$priv"); psk=$(wg genpsk)
        addr4="$NET4.$n"; addr6="${NET6}$(printf '%x' "$n")"
        printf '# user: %s\n[Peer]\nPublicKey = %s\nPresharedKey = %s\nAllowedIPs = %s/32, %s/128\n\n' "$2" "$pub" "$psk" "$addr4" "$addr6" >> "$CFG"
        sync
        cat > "$CLIENTS/$2.conf" <<EOF
[Interface]
PrivateKey = $priv
Address = $addr4/32, $addr6/128
DNS = $DNS

[Peer]
PublicKey = $(server_pubkey)
PresharedKey = $psk
Endpoint = $ENDPOINT
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = $KEEPALIVE
EOF
        echo "added $2 ($addr4, $addr6)" >&2
        show_conf "$2" ;;
    show)
        [ $# -eq 2 ] || die "usage: wg-user show <name>"; has_user "$2" || die "no user $2"
        show_conf "$2" ;;
    del)
        [ $# -eq 2 ] || die "usage: wg-user del <name>"; has_user "$2" || die "no user $2"
        pub=$(peer_pubkey "$2")
        cp -p "$CFG" "$CFG.$(date +%Y%m%d-%H%M%S)"
        awk -v n="# user: $2" '$0==n{skip=1} !skip{print} skip&&/^$/{skip=0}' "$CFG" > "$CFG.new" && mv "$CFG.new" "$CFG"
        [ -n "$pub" ] && wg set "$WG_IF" peer "$pub" remove
        rm -f "$CLIENTS/$2.conf"
        echo "removed $2" >&2 ;;
    *) die "usage: wg-user list | add <name> | show <name> | del <name>" ;;
esac

Что он делает при add:

  • генерирует ключи клиента и preshared key (wg genpsk) — дополнительный общий секрет на пару «сервер — этот клиент». Он не обязателен, но добавляет к криптографии на открытых ключах симметричный слой: даже если когда-нибудь появится способ вычислить закрытый ключ из открытого, трафик с PSK останется закрытым;
  • выбирает адрес: первый свободный в 10.66.66.0/24, и тот же номер в IPv6-сети;
  • дописывает [Peer] на сервер — открытый ключ клиента, PSK и его адреса в AllowedIPs, с комментарием # user: имя, по которому скрипт потом находит блок;
  • применяет без перезапуска: wg syncconf загружает в работающий интерфейс разницу между текущим состоянием и конфигом, остальные клиенты ничего не замечают. wg-quick strip убирает из конфига поля, которые понимает только wg-quick (Address, DNS), а не wg;
  • собирает конфиг клиента и показывает его текстом и QR-кодом — телефон его отсканирует с экрана.

Клиентский конфиг выглядит так:

[Interface]
PrivateKey = <закрытый ключ клиента>
Address = 10.66.66.2/32, fd66:6666:6666::2/128
DNS = 1.1.1.1, 2606:4700:4700::1111

[Peer]
PublicKey = <открытый ключ сервера>
PresharedKey = <общий секрет>
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

По строкам:

  • Address — адреса клиента в туннеле, /32 и /128: одно устройство, одна пара адресов.
  • DNS — какие резолверы использовать, пока туннель поднят. Запросы к ним пойдут через туннель (адреса покрыты AllowedIPs), и провайдер клиента не увидит имён сайтов. Без этой строки клиент продолжит спрашивать DNS провайдера или роутера, и это та самая утечка DNS из двадцатой части. Можно указать свой резолвер на сервере, если он там есть.
  • Endpoint — куда подключаться. Здесь адрес, а не имя: тогда подключение не зависит от DNS. Имя тоже можно, клиент разрешит его при запуске.
  • AllowedIPs = 0.0.0.0/0, ::/0 — полный туннель: весь трафик, IPv4 и IPv6, через сервер. Для раздельного туннеля сюда пишут только те сети, которые должны идти через VPN, например 10.66.66.0/24, 192.168.100.0/24 — сеть туннеля и сеть за сервером; всё остальное пойдёт напрямую. Клиенты WireGuard строят маршруты ровно по этому списку.
  • PersistentKeepalive = 25 — раз в 25 секунд отправлять серверу пустой пакет. Нужен, когда клиент за NAT, то есть почти всегда: домашний роутер и оператор мобильной связи помнят UDP-поток 30–60 секунд после последнего пакета и потом забывают; тогда ответ сервера, пришедший первым после паузы, пропадёт. Keepalive не даёт записи протухнуть. Цена — чуть больше расхода батареи на телефоне; на сервере эта строка не нужна.

Пользуются скриптом так:

wg-user add laptop           # конфиг и QR-код на экран
wg-user add phone
wg-user list                 # кто есть, откуда подключён, когда последнее рукопожатие
wg-user show phone           # QR-код ещё раз
wg-user del laptop           # отозвать: туннель ноутбука падает сразу, телефон работает

Конфиг клиента — секрет: в нём закрытый ключ. Передавайте его на устройство так, чтобы он не остался в переписке: QR-код с экрана, файл по SSH, менеджер паролей.


Клиент на Windows

Официальный клиент — WireGuard for Windows, с сайта wireguard.com/install или одной командой:

winget install WireGuard.WireGuard

Он состоит из службы, драйвера виртуального адаптера и окна управления; работает на Windows 10 и 11, ставится за полминуты, запрашивает права администратора.

Импорт туннеля. Самый простой путь — файл конфига. Скопируйте текст, который напечатал wg-user add laptop, в файл с расширением .conf, например laptop.conf (имя файла станет именем туннеля, до 15 символов: латиница, цифры, _, -, =, +, .). В окне WireGuard: Add Tunnel → Import tunnel(s) from file… и выберите файл. Или Add empty tunnel… и вставьте текст целиком в поле. Клиент покажет туннель слева в списке, справа — его параметры с открытым ключом; закрытый ключ он хранит в зашифрованном виде и в окне не показывает. Файл laptop.conf после импорта удалите.

Подключение — кнопка Activate. Через секунду статус станет Active, а в блоке Peer появятся Latest handshake и счётчики Transfer. Если рукопожатия нет и счётчик received стоит на нуле — пакеты до сервера не доходят или сервер не принимает: проверьте порт в файрволе сервера и совпадение ключей.

Kill switch. Правой кнопкой по туннелю → Edit: когда в AllowedIPs стоит 0.0.0.0/0, ::/0, внизу окна появляется галочка Block untunneled traffic (kill-switch). Она добавляет правила файрвола Windows, которые запрещают любой трафик мимо туннеля, кроме пакетов к самому серверу и DHCP. Пока туннель активен и упал, в интернет ничего не уйдёт в открытую. Для ноутбука в кафе — то, что нужно; дома, где туннель включают ради одного сайта, можно не ставить.

Что происходит в системе — в PowerShell:

PS C:\> Get-NetAdapter | Where-Object InterfaceDescription -like 'WireGuard*'

Name    InterfaceDescription   Status  MacAddress  LinkSpeed
----    --------------------   ------  ----------  ---------
laptop  WireGuard Tunnel       Up                  100 Gbps

PS C:\> Get-NetIPAddress -InterfaceAlias laptop | Select-Object IPAddress, PrefixLength

IPAddress            PrefixLength
---------            ------------
fd66:6666:6666::2             128
10.66.66.2                     32

PS C:\> Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object InterfaceAlias, NextHop, RouteMetric, InterfaceMetric

InterfaceAlias NextHop     RouteMetric InterfaceMetric
-------------- -------     ----------- ---------------
laptop         0.0.0.0               0               0
Ethernet       192.168.1.1           0              25

Два маршрута по умолчанию: старый через роутер и новый через туннель. Побеждает тот, у которого сумма метрик меньше, — клиент WireGuard ставит своему интерфейсу метрику 0, и весь трафик уходит в него. Отдельного маршрута к самому серверу 203.0.113.10 в таблице не ищите, его нет: зашифрованные пакеты к серверу клиент отправляет через сокет, привязанный к физическому интерфейсу, и таблица маршрутов для них не применяется. Иначе они попали бы в туннель, были бы зашифрованы ещё раз и так до бесконечности. На Linux wg-quick решает ту же задачу через policy routing: маркирует свои пакеты (fwmark) и правилом ip rule пускает всё, кроме помеченных, через отдельную таблицу с маршрутами туннеля. DNS интерфейса — 1.1.1.1 из конфига; клиент к тому же блокирует запросы DNS через другие интерфейсы, так что утечки нет.

Автозапуск. Туннель, оставленный активным, поднимется сам при следующей загрузке: служба запоминает состояние. Отдельная настройка не нужна.

Можно обойтись и без окна: wireguard.exe /installtunnelservice C:\path\laptop.conf ставит туннель службой, wireguard.exe /uninstalltunnelservice laptop убирает. Это пригодится, если туннели нужно раздавать скриптом на много машин.


Клиент на Android

Приложение называется WireGuard, автор WireGuard Development Team, есть в Google Play и в F-Droid. Работает на Android 5 и новее; на телефонах без модуля в ядре — в пользовательском режиме, медленнее, но так же.

Импорт. Нажмите синий + внизу справа. Три варианта: Scan from QR code — камеру на QR-код, который напечатал wg-user add phone (прямо с экрана терминала, экран ноутбука читается с полуметра); Import from file or archive — файл .conf, если передали его на телефон; Create from scratch — вбить руками. После сканирования приложение спросит имя туннеля, дальше он появится в списке.

Подключение — переключатель справа от имени туннеля. Android спросит разрешение на создание VPN-подключения — один раз, нажмите OK. В строке состояния появится ключик. Нажмите на имя туннеля — раскроются параметры: адреса, DNS, пир, Latest handshake и Transfer. Пока рукопожатия нет, туннель не работает.

Исключения для приложений. В настройках туннеля (карандаш) → All Applications можно исключить приложения из туннеля или, наоборот, пустить через него только выбранные. Банковские приложения, которые не любят VPN, или местный видеосервис, который через зарубежный сервер не покажет контент, — сюда.

Always-on VPN. Чтобы туннель поднимался при включении телефона и не падал, когда система убивает приложение в фоне: Настройки Android → Сеть и интернет → VPN → шестерёнка рядом с WireGuard → Always-on VPN. Там же Block connections without VPN — аналог kill switch: без туннеля интернета нет вовсе. У разных производителей путь к этому экрану немного отличается, но называется он везде одинаково.

Батарея. Keepalive 25 секунд не даёт радиомодулю спать подолгу, и в день это несколько процентов заряда. Если телефон не за NAT (редкость) или вы готовы к секундной задержке при первом пакете после простоя, keepalive можно убрать; на Android, где система сама отправляет приложение спать, обычно оставляют.


Смотрим руками

На сервере. Кто подключён и когда последний раз подавал признаки жизни:

# wg show wg0
interface: wg0
  public key: <открытый ключ сервера>
  private key: (hidden)
  listening port: 51820

peer: <открытый ключ laptop>
  preshared key: (hidden)
  endpoint: 198.51.100.7:53421
  allowed ips: 10.66.66.2/32, fd66:6666:6666::2/128
  latest handshake: 48 seconds ago
  transfer: 14.21 MiB received, 203.56 MiB sent
  persistent keepalive: every 25 seconds

endpoint — откуда пришёл последний пакет клиента: публичный адрес его роутера и порт NAT. latest handshake старше двух-трёх минут при живом клиенте — пакеты не ходят. wg-user list показывает то же с именами.

Пакеты как их видит сеть:

# tcpdump -ni eth0 udp port 51820 -c 4
IP 198.51.100.7.53421 > 203.0.113.10.51820: UDP, length 148
IP 203.0.113.10.51820 > 198.51.100.7.53421: UDP, length 92
IP 198.51.100.7.53421 > 203.0.113.10.51820: UDP, length 128
IP 203.0.113.10.51820 > 198.51.100.7.53421: UDP, length 1392

Первые два — рукопожатие: запрос ровно 148 байт, ответ ровно 92. Дальше — данные, у которых к длине пакета внутри добавлены 32 байта заголовка WireGuard. Содержимого не видно, но факт WireGuard виден любому, кто смотрит на трафик: по этим двум длинам его узнают и файрволы, и DPI. Если задача — не только защитить трафик, но и не дать понять, что это VPN, нужен другой инструмент: об этом отдельная статья.

Транзит и NAT в действии — счётчики правил:

# iptables -L FORWARD -nv
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target  prot opt in     out     source       destination
 3012  211K ACCEPT  0    --  wg0    eth0    0.0.0.0/0    0.0.0.0/0
 4188 4967K ACCEPT  0    --  eth0   wg0     0.0.0.0/0    0.0.0.0/0    ctstate RELATED,ESTABLISHED
    5   300 DROP    0    --  wg0    *       0.0.0.0/0    0.0.0.0/0

# iptables -t nat -L POSTROUTING -nv
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target      prot opt in     out     source          destination
  164 11K   MASQUERADE  0    --  *      eth0    10.66.66.0/24   0.0.0.0/0

Пять пакетов в DROP — чьи-то попытки из туннеля достучаться до адресов, которые не ведут в интернет через eth0. В таблице nat счётчик меньше, чем в FORWARD: NAT-правило срабатывает один раз на соединение, дальше conntrack подменяет адреса сам.

На клиенте. Куда вы выходите:

curl.exe -4 https://ifconfig.co      # 203.0.113.10
curl.exe -6 https://ifconfig.co      # 2001:db8::10

Оба адреса — сервера, значит, ни IPv4, ни IPv6 мимо туннеля не идут. Утечку DNS проверяет любой сайт проверки DNS leak: резолвер в ответе должен быть тем, что в строке DNS, а не провайдерским. На Windows Resolve-DnsName whoami.akamai.net вернёт адрес резолвера, который пришёл к Akamai с вашим запросом.

MTU. Заголовок WireGuard плюс UDP плюс IP — 60 байт для IPv4 и 80 для IPv6, поэтому интерфейс туннеля по умолчанию получает MTU 1420 вместо 1500 (шестая часть о том, что это и чем грозит). Если клиент сам сидит за PPPoE с MTU 1492 (четвёртая часть) или за ещё одним туннелем, 1420 может оказаться много: сайты открываются, а большие страницы и загрузки виснут. Тогда в [Interface] клиента добавляется MTU = 1380, и у wg-quick, и у клиентов Windows и Android эта строка понимается.


Если не работает

  • Нет рукопожатия, received 0. До сервера не доходят пакеты или сервер их отбрасывает. Проверить: слушает ли сервер (ss -ulnp | grep 51820), пропускает ли его файрвол (iptables -S INPUT), файрвол хостинга (у облаков часто свой, снаружи машины), и совпадают ли ключи — открытый ключ сервера в конфиге клиента с wg show wg0, открытый ключ клиента в [Peer] сервера с тем, что показывает клиент. Один неверный символ — тишина без единого сообщения об ошибке: WireGuard не отвечает на пакеты, которые не может проверить.
  • Рукопожатие есть, интернета нет. Туннель работает, сервер не пересылает. Проверить sysctl net.ipv4.ip_forward (должно быть 1), правило MASQUERADE в iptables -t nat -S и транзитные правила в iptables -S FORWARD: если их нет, PostUp не выполнился — journalctl -u wg-quick@wg0 покажет, на какой команде. Для IPv6 — то же с ip6tables и net.ipv6.conf.all.forwarding.
  • Работает минуту, потом отваливается, потом снова работает. Клиент за NAT без PersistentKeepalive. Добавить PersistentKeepalive = 25 в [Peer] клиента.
  • Сайты открываются, большие файлы и видео виснут. MTU, см. выше.
  • Через туннель видно IPv4-адрес сервера, а IPv6 — провайдера. В AllowedIPs клиента нет ::/0, или на сервере нет IPv6-адреса на wg0 и NAT66. Либо добавить, либо выключить IPv6 на клиенте на время туннеля.
  • Время на сервере или клиенте сильно врёт. WireGuard защищается от повторного воспроизведения старых рукопожатий по счётчику, в который входит время. Если часы на сервере после переустановки отстают на год, рукопожатия от клиентов будут отвергаться как «старые». Синхронизация времени — timedatectl на сервере.

Что получилось

Сервер WireGuard — это десять строк конфига, один UDP-порт и несколько правил iptables для транзита и NAT, которые живут вместе с интерфейсом. Устройство — пара ключей и секция [Peer]; скрипт делает это одной командой и печатает QR-код. Клиент на Windows импортирует файл и умеет kill switch, клиент на Android сканирует QR-код и живёт в режиме always-on. Весь трафик, IPv4 и IPv6, и запросы DNS идут через сервер; отзыв устройства — одна команда, не трогающая остальные.

Чего WireGuard не делает: не прячет сам факт VPN. В сети, где VPN режут по сигнатурам, нужна маскировка под обычный HTTPS, и это тема статьи про Xray и REALITY. А там, где VPN никому не мешает, — в кафе, в гостинице, в поездке, — WireGuard сегодня самый простой способ сделать так, чтобы ваш трафик видел только ваш сервер.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.