Разные IP https://www.reg.ru/web-tools/myip https://2ip.ru/ Разные реализации WG для macos: https://github.com/cloudflare/boringtun https://git.zx2c4.com/wireguard-go/about/ https://tunsafe.com/user-guide/linux --------- # Разбираемся с WireGuard и делаем свой умный VPN IMG_1589.PNG Сейчас в РФ сложилась забавная ситуация: с одной стороны, РКН блокирует довольно много сайтов, к которым иногда нужен доступ, а с другой стороны, из-за DDOS-атак, многие крупные компании ограничили доступность своих сайтов из-за границы: например, pochta.ru, leroymerlin.ru, rt.ru, avito.ru, не открываются через VPN. Получается ситуация из мема: — Нужен твиттер! — Включить VPN! — А теперь заказать еду! — Выключить VPN! — А теперь выложить еду в инстаграм! — Включить VPN! Каждый с этим борется как может. Например, на iphone [родными средствами](https://superg.ru/kak-nastroit-avtomaticheskoe-vklyuchenie-vpn-na-iphone/) можно настроить автоматизацию, которая будет запускать VPN при открытии определенных приложений (например, твиттера), а при выходе из них — выключать обратно. Но на одной из картинок я видел, что последнюю реплику заменяют на "Включить VPN, но чуть-чуть!". Вот тем, что сейчас будем "включать чуть-чуть VPN" мы и займемся. Примечание душнилы: я прекрасно понимаю, что при таком использовании это скорее прокси, а не VPN, но с языком ничего не поделать — если все называют программы для обхода блокировок VPN, то лучшее, что можно тут сделать — постараться привыкнуть и получать удовольствие. Заодно чуть улучшим качество связи с локальными ресурсами: необходимость таскать трафик сначала до VPN вне страны, а потом обратно до сервера внутри ее драматично сказывается если не на скорости, то на задержке точно: даже на проводном интернете пинг в 4мс до яндекса легко превращается в 190мс, а на мобильном интернете — из 80мс в 240. Дополнительный хоп чуть ухудшит ситуацию, но далеко не так драматично. Делать мы это будем на основе WireGuard — это относительно новая (разрабатыавется с 2016 года в отличии от OpenVPN (2002) и IPsec (где-то в том же районе)) технология VPN, которая была создана, по сути, одним человеком — zx2c4, которого в миру зовут Джейсоном Доненфельдом. Плюсы WG — скорость (особенно для Linux, где он может работать как модуль ядра начиная с 5.6 и Windows, где модуль для ядра выпустили порядка недели назад), низкие задержки, современная криптография, и простое использование конечным юзером. Ах да, еще UDP. UDP для туннелей это хорошо, потому что у TCP уже есть механизмы, которые позволяют ему работать на неидеальных соединениях, а UDP представляет из себя именно такое соединение. А когда вы засовываете TCP в TCP, то отказываетесь от большей части этих механизмов (инкапсулированный TCP-пакет будет гарантированно доставлен другой стороне, хотя протокол допускает недоставку), но все еще несете весь оверхед вида "хендшейк соединения для отправки хендшейка". Особенно для одинокого пользователя-хакера приятна работа с шифрованием: нет ни необходимости в сертификатах и удостоверяющих центрах, ни в логинах-паролях, все, что нужно — это с тем пиром, с которым хотите установить соедиение, передать друг другу публичные ключи вашего интерфейса WG. Для больших компаний, это, конечно, будет скорее минусом, как и то, что WG — это только базовая часть полноценной большой инфраструктуры VPN. Но, например, именно WireGuard использовали в Cloudflare для своего WARP (https://blog.cloudflare.com/announcing-warp-plus/, https://blog.cloudflare.com/warp-technical-challenges/), правда, написав его собственную реализацию — boringtun. Еще одним минусом WG является то, что трафик не обфусцирован — DPI может обнаружить трафик WireGuard, так что его можно довольно легко заблокировать (не говоря уж о блокировке UDP совсем, что почти не мешает вебу, но гарантированно ломает работу WireGuard). Для скрытия трафика рекомендуется использовать специализированное ПО — Cloak, Obfsproxy, Shadowsocks, Stunnel, SoftEther, SSTP, в конце-концов, SSH. Если очень упрощать, ключи работают следующим образом: у нас есть закрытый (приватный) ключ, из которого можно сгенерировать открытый, или публичный. Наоборот — нельзя, из открытого ключа мы получить закрытый никак не можем. После чего, мы можем зашифровать с помощью закрытого ключая какую-то строку, а при помощи открытого — расшифровать ее и тем самым убедиться, что у собеседника точно есть закрытый ключ, а значит, он тот, за кого себя выдает. Таким образом, мы можем без проблем публиковать открытый ключ — он всего лишь позволяет проверить подлинность автора, но не притвориться им. Это как в SSH — публичный ключ лежит на сервере, где его потеря небольшая беда: все что сможет сделать с ним злоумышленник это положить его на свой сервер, чтобы вы к нему могли подключиться с помощью закрытого ключа. Так вот, в WG первый этап подключения заключается в том, что каждая сторона с помощью зашифрованного приватным ключом сообщения доказывает собеседнику, что она именно она: это проверяется публичным ключом. Второй этап — это создание симметричных ключей для шифрования самого трафика. ## Шаг первый: создаем и настраиваем два сервера. Один внутри страны — через него будет идти трафик на локальные ресурсы, а второй за границей. Далее я их буду называть **local** и **external**. Идеально, если **local** будет в вашей домашней сети, потому что при этом трафик на локальные ресурсы не будет отличаться от вашего домашнего трафика. Но для этого нужен какой-то хост дома, белый IP и возможность пробросить порт. У меня это виртуалка на домашнем сервер, но навереное, подойдет и малина (не пробовал, ей придется маршрутизировать весь трафик с устройств и держать в памяти ~11к маршрутов). Если дома хоста нет, то можно взять любой сервер у VDS-хостера (vdsina, ruvds), но могут быть проблемы у ресурсов-параноиков, которые блокируют подсети хостеров, полагая, что серверам их ресурс не нужен: на vdsina я такое ловил. Внешний сервер можно взять у тех же хостеров VDS, что и выше: у них есть зарубежные площажки, а можно выбрать иностранного хостера. Например, у меня 1984.hosting. Считаем, что на обоих серверах у нас Debian 11. Ставим нужные нам пакеты: ```apt update && apt install wireguard iptables ipcalc qrencode curl jq traceroute dnsutils ufw -y``` Включаем перенаправление трафика: в этом случае сервер, получив пакет, который предназначается ни одному из его адресов, не отбросит его, а попытается перенаправить в соответствии со своими маршрутами. ```bash echo "net.ipv4.ip_forward=1" > > /etc/sysctl.conf``` echo "net.ipv4.conf.all.forwarding=1" > > /etc/sysctl.conf sysctl -p /etc/sysctl.conf ``` Опционально (но очень удобно) сразу поменять hostname обоих серверов, чтобы не запутаться, где какая консоль: ```bash hostnamectl set-hostname trickster-internal hostnamectl set-hostname trickster-external ``` ### Шаг второй: настраиваем WireGuard для связи двух серверов: Для начала генерируем ключи. Запускаем два раза **wg genkey** и получаем два приватных ключа: ```bash root@trikster-internal:~# wg genkey kOd3FVBggwpjD3AlZKXUxNTzJT0+f3MJdUdR8n6ZBn8= root@trikster-internal:~# wg genkey 6CCRP42JiTObyf64Vo0BcqsX6vptsqOU+MKUslUun28= ``` Утилита wg genkey не делает никакой магии, это просто аналог чего-то типа "```echo $RANDOM | md5sum | head -c 32 | base64```", только наверняка более криптостойкое: мы просто генерируем 32 байта случайных значений и представляем их в виде base64. Создаем два конфига: На **internal**: ```/etc/wireguard/wg-internal.conf``` ```ini [Interface] Address = 10.20.30.1/32 ListenPort = 17968 PrivateKey = kOd3FVBggwpjD3AlZKXUxNTzJT0+f3MJdUdR8n6ZBn8= PostUp = iptables -t nat -A POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE PostUp = ip rule add from `ip route | awk '/default/ {print $3; exit}'` table main PostDown = iptables -t nat -D POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE PostDown = ip rule del from `ip route | awk '/default/ {print $3; exit}'` table main ``` На **external**: ```/etc/wireguard/wg-external.conf``` ```ini [Interface] Address=10.20.30.2/32 PrivateKey=6CCRP42JiTObyf64Vo0BcqsX6vptsqOU+MKUslUun28= PostUp = iptables -t nat -A POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE PostDown = iptables -t nat -D POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE ``` Секция Interface — это настройки конкретного сетевого интерфейса Wireguard, того, что будет виден в ```ip a```. Название интерфейса берется из название текущего файла конфигурации. У одного интерфейса всегда одна ключевая пара: у пиров этого интерфейса одинаковый публичный ключ. Но никто не мешает, если хочется, сделать под каждого пира отдельный конфиг-файл, и отдельный интерфейс (правда, на сотнях клиентов это будет неудобно). Управляются интерфейсы обычно при помощи утилиты **wg-quick**: ```wg-quick down wg-external``` и ```wg-quick up wg-external``` Утилита **wg-quick** — это, на самом деле, 400 строк на баше, которые автоматизируют частоиспользуемые вещи: например, установку маршрутов. Сам факт туннеля не делает ничего, кроме создания "трубы" за которой находится другой пир. Для того, чтобы ваш запрос в браузере попал в интерфейс, системе надо явно сказать "маршрутизируй, пожалуйста, пакеты с таким-то адресом назначения вот в этот сетевой интерфейс". Именно этим занимается **wg-quick**. Ну еще и настройкой DNS, указанных в конфиге и установкой MTU. Но ничего сложного в этом нет, достаточно сделать "```cat /usr/bin/wg-quick```", чтобы посмотреть на эту логику, и если надо, сделать тоже самое руками. **Interface-Address** — это IP текущего пира. Вся адерсация в WG статическая. С одной стороны, это упрощает настройку и бутстрап, с другой стороны, усложняет работу, если у вас очень много клиентов. **ListenPort** — это UDP-порт для подключения извне. Если не указать, будет прослушивать 51820. Если этот пир будет только подключаться к другим клиентам, можно и не использовать. **Interface-PostUp** и **PostDown** — это скрипты, выполняющиеся после поднятия и после остановки интерфейса. Есть еще **PreUP** и **PreDown**. Кроме публичных и приватных ключей есть еще опция **PresharedKey**, которая обеспечивает дополнительное шифрование симметричным шифром. Ключ генерируется командой ```wg genpsk``` и кладется в **PresharedKey** в секциях **Peer** на обоих пирах. Неиспользование этой опции не снижает нагрузку по шифровке-расшифровке: если ключ не указан, используется нулевое значение ключа. Для действительного обеспечения пост-квантовой безопасности (невозможности расшифровки данных квантовыми компьютерами) разработчики рекомендуют дополнительный внешний квантово-устойчивый механизм хендшека (например, Microsoft SIDH, который они пиарят именно в таком контексте), чей найденный общий ключ можно использовать в качестве **PresharedKey**. Заклинания в PostUp достаточно просты. ````ip route | awk '/default/ {print $5; exit}'```` — это команда для подстановки имени сетевого интерфейса, куда по-умолчанию выполняется маршрутизация: как правило, это тот интерфейс, в который воткнут провайдер или роутер. ````ip route | awk '/default/ {print $3; exit}'```` — тоже самое, но подставляет IP-адрес дефолтного маршрута. Таким образом, первая страшная команда упрощается и превращается в ```iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE```, которая представляет собой включение NAT в режиме маскарада: сервер будет отправлять пришедшие ему пакеты пакеты во внешнюю сеть, подменяя в них адрес отправителя на свой, чтобы ответы на эти пакеты тоже приходили ему, а не исходному отправителю. Вторая страшная команда превращается в ```ip rule add from 95.93.219.123 table main``` — это необходимо для сервера **internal**, потому что иначе при активации маршрута 0.0.0.0/0 он начинает пересылать ответы на пакеты, приходящие ему на внешние адреса через туннель WG. Сервер на том конце, конечно, пересылает их по назначению, но тут уже не готов отправитель пакета: он присылает что-то на внешний адрес сервера **internal**, а ответ ему приходит с **external**. Естественно, при включенном rp_filter пакет отбрасывается. В этом случае сервер перестает быть доступен, например, по SSH снаружи, к нему надо коннектиться только по внутреннему IP wireguard-а. Отключать rp_filter у сервера это из пушки по воробьям, а вот дополнительное правило исправляет ситуацию. Продолжим писать конфиг: в него надо добавить секцию **Peer**, чтобы связать их с друг-другом. Я намеренно не привожу сразу готовые конфиги, потому что хочу показать механизм создания конфигов в ручном режиме — в свое время у меня были проблемы с тем, что я генерировал конфиги утилитами типа ```easy-wg-quick```, которые спрашивают тебя о названии клиента и красиво показывают QR-код прям в консоли, но отнюдь не способствуют пониманию того, как работает WG на самом деле. Итак, добавляем в каждый по секции **Peer**, для чего генерируем из приватного ключа публичный (вот в pubkey как раз происходит крипто-магия): ```bash echo "kOd3FVBggwpjD3AlZKXUxNTzJT0+f3MJdUdR8n6ZBn8=" | wg pubkey MxnOnIlKfSyZyRutnYyoWHb3Izjalgf1t8F1oPJiyyw= ``` Это публичный ключ сервера internal, его мы помещаем в секцию **peer** на **external**: ```/etc/wireguard/wg-external.conf``` ```ini [Peer] PublicKey=MxnOnIlKfSyZyRutnYyoWHb3Izjalgf1t8F1oPJiyyw= AllowedIPs=10.20.30.0/24 Endpoint=195.2.79.13:17968 PersistentKeepalive=25 ``` Там же, в **Endpoint** указываем адрес сервера **internal** и порт, который мы задали в **ListenPort**. С **AllowedIPs** при использовании ```wg-quick``` возникает небольшая путаница: это изначально именно, то как оно называется — список разрешенных IP-адресов к приему из туннеля: если что-то прилетает с другим src, оно будет отброшено. Но при использовании ```wg-quick``` она разумно считает, что если там есть какие-то устройства, которые могут послать пакет, то значит пакеты к этим устройствам надо маршрутизировать туда же, и создает маршруты на эти адреса, указывающие на туннель пира. В данных примерах **AllowedIPs** можно читать как "адреса, трафик на которые будут маршрутизироваться в туннель этого пира и с которых пир сможет отправить что-то в туннель". Т.е. пункт "```AllowedIPs = 10.20.30.3/32```" означает, буквально, "только запросы на 10.20.30.3 (адрес пира WG) отправлять в туннель", т.е. дать доступ только до машины этого клиента. Пункт "```AllowedIPs = 192.168.88.0/24```" означает, что при запросе адреса из этой подсети, этот запрос уйдет в туннель клиента, и если у него включен форвардинг и ему доступна эта подсеть, то к ней можно будет получить доступ. А "```AllowedIPs = 0.0.0.0/0```" означает, что в туннель надо маршрутизировать весь трафик вообще. Правда, это не относится к трафику, например, локальной сети: приоритет у маршрута, который создастся из маски подсети и адреса шлюза, выше чем у 0.0.0.0/0. Также, маршрут 0.0.0.0/0 перебьют маршруты других пиров, если они будут в конфиге. В данном случае "```AllowedIPs=10.20.30.0/24```" — означает что трафик с **external** в подсеть 10.20.30.0-10.20.30.255 будет уходить в туннель к **internal**. В принципе, нужды в этом особо нет, **external** у нас исключительно выходная нода. Но вдруг мы как-нибудь захотим зайти оттуда по ssh на какую-нибудь другую машину. Повторяем генерацию публичного ключа с **external**: ```bash echo "6CCRP42JiTObyf64Vo0BcqsX6vptsqOU+MKUslUun28=" | wg pubkey FulnUTovyyfgn5kmgPkcj2OjKRFGeLkaTsHtAOy6HW8= ``` Мы получаем публичный ключ сервера **external** и помещаем его в секцию peer сервера internal: ```/etc/wireguard/wg-internal.conf``` ```ini [Peer] #external node PublicKey = FulnUTovyyfgn5kmgPkcj2OjKRFGeLkaTsHtAOy6HW8= AllowedIPs = 10.20.30.2/32, 0.0.0.0/0 ``` AllowedIPs тут ```10.20.30.2/32, 0.0.0.0/0``` — указываем, что за туннелем находится конкретный IP 10.20.30.2 и помимо этого, пробрасываем весь трафик, не связанный другими маршрутами, в этот туннель: **external** у нас это основная выходная нода нашего VPN, так что по умолчанию весь трафик будет направляться через нее, т.к. зарубежных маршрутов больше, чем российских, и логичнее фильтровать именно российские, а зарубежный трафик пустить по умолчанию через ноду в другой стране. Итак, два конфига: `/etc/wireguard/wg-internal.conf` ```ini [Interface] Address = 10.20.30.1/32 ListenPort = 17968 PrivateKey = kOd3FVBggwpjD3AlZKXUxNTzJT0+f3MJdUdR8n6ZBn8= PostUp = iptables -t nat -A POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE PostUp = ip rule add from `ip route | awk '/default/ {print $3; exit}'` table main PostDown = iptables -t nat -D POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE PostDown = ip rule del from `ip route | awk '/default/ {print $3; exit}'` table main #external node [Peer] PublicKey = FulnUTovyyfgn5kmgPkcj2OjKRFGeLkaTsHtAOy6HW8= AllowedIPs = 10.20.30.2/32, 0.0.0.0/0 #mobile-client node [Peer] PublicKey = 26Vhud00ag/bdB9molvSxfBzZTlzdO+aZgrX3ZDncSg= AllowedIPs = 10.20.30.3/32 ``` ```/etc/wireguard/wg-external.conf``` ```ini [Interface] Address=10.20.30.2/32 PrivateKey=6CCRP42JiTObyf64Vo0BcqsX6vptsqOU+MKUslUun28= PostUp = iptables -t nat -A POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE PostDown = iptables -t nat -D POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE #internal node [Peer] PublicKey=MxnOnIlKfSyZyRutnYyoWHb3Izjalgf1t8F1oPJiyyw= AllowedIPs=10.20.30.0/24 Endpoint=195.2.79.13:17968 PersistentKeepalive=25 ``` Теперь можно поднять туннели на обоих серверах: ```bash wg-quick down wg-external ; wg-quick up wg-external wg-quick down wg-internal ; wg-quick up wg-internal ``` Проверяем, что туннели активны командой wg: ```bash root@trikster-internal:~# wg ... peer: FulnUTovyyfgn5kmgPkcj2OjKRFGeLkaTsHtAOy6HW8= endpoint: 51.159.187.77:36276 allowed ips: 10.20.30.2/32, 0.0.0.0/0 latest handshake: 13 seconds ago transfer: 180 B received, 92 B sent root@trikster-external:~# wg ... peer: MxnOnIlKfSyZyRutnYyoWHb3Izjalgf1t8F1oPJiyyw= endpoint: 195.2.79.13:17968 allowed ips: 10.20.30.0/24 latest handshake: 10 seconds ago transfer: 92 B received, 180 B sent persistent keepalive: every 25 seconds ``` Если видим "latest handshake" и байты и в received и в sent, значит, все ок. Если ,байты только в send, без хендшейка и полученных данных, где-то в ошибка в конфиге или сервера недоступны друг для друга. Если что-то пошло не так, и отвалился ssh, то достаточно перезагрузить сервер. Если все хорошо, и доступ к серверам сохранился, ставим туннели в автозапуск: ```bash systemctl enable wg-quick@wg-external.service systemctl enable wg-quick@wg-internal.service ``` Попробуем посмотреть маршрут (рекомендую замечательную утилиту ```mytraceroute```, ```mtr```) без туннеля: ```bash root@trikster-internal:~# wg-quick down wg-internal && sleep 10 && mtr -r google.com HOST: trikster-internal.local Loss% Snt Last Avg Best Wrst StDev 1.|-- host-89-22-232-243.hosted 0.0% 10 0.3 5.4 0.3 49.8 15.6 2.|-- 172.31.0.1 0.0% 10 0.3 19.8 0.3 122.2 42.6 3.|-- 109.239.138.90 0.0% 10 1.5 1.9 1.4 3.0 0.6 4.|-- 91.108.51.4 0.0% 10 11.4 11.4 11.3 11.7 0.1 5.|-- 178.18.227.12.ix.dataix.e 0.0% 10 11.0 17.9 11.0 77.0 20.8 ``` И с туннелем: ```bash root@trikster-internal:~# wg-quick up wg-internal && sleep 10 && mtr -r google.com HOST: trikster-internal.local Loss% Snt Last Avg Best Wrst StDev 1.|-- 10.20.30.2 0.0% 10 51.3 51.3 51.2 51.4 0.1 2.|-- 10.200.100.0 0.0% 10 51.4 51.4 51.2 51.6 0.1 3.|-- 10.197.37.65 0.0% 10 52.5 52.2 52.0 52.5 0.2 4.|-- 10.197.0.41 0.0% 10 52.2 52.2 52.1 52.5 0.1 5.|-- 10.197.0.44 0.0% 10 52.0 52.2 51.9 52.4 0.1 ``` Все хорошо, трафик идет через внешний сервер — сначала на 10.20.30.2, который у нас выходная нода, а потом через его маршрутизаторы. ### Шаг третий: добавляем конфиг клиента Создаем конфиг клиента, конечного устройства-пользователя VPN. За основу берем ```wg-external.conf```, потому что он такой же точно клиент, который подключается к internal, разница только в том, что **external** получает пакеты, а наш клиент будет отправлять. Генерируем ему сразу пару публичный-приватный ключ: ```bash prk=`wg genkey` && pbk=`echo $prk | wg pubkey` && printf "Private: $prk\nPublic: $pbk\n" Private: iPK7hYSU8TLVRD+w13nd3aGSYNLfnTx6zwdRzKcGb1o= Public: 26Vhud00ag/bdB9molvSxfBzZTlzdO+aZgrX3ZDncSg= ``` Конфиг почти такой же: ```/etc/wireguard/wg-notebook-client.conf``` ```ini [Interface] Address = 10.20.30.3/32 PrivateKey = iPK7hYSU8TLVRD+w13nd3aGSYNLfnTx6zwdRzKcGb1o= DNS = 1.1.1.1, 8.8.8.8 #internal node [Peer] PublicKey = MxnOnIlKfSyZyRutnYyoWHb3Izjalgf1t8F1oPJiyyw= AllowedIPs = 0.0.0.0/0 Endpoint = 195.2.79.13:17968 PersistentKeepalive = 25 ``` Тут у нас добавилась опция **PersistentKeepalive**. Дело в том, что роутеры в цепочке между двумя пирами ничего не знают о сессии WG, а знают только о потоке UDP-пакетов. Для маршрутизации UDP-пакетов за NAT они создают у себя табличку, в которой записывают, кто куда и на какой порт отправил пакет. И если с destination-адреса/порта приходит UPD-пакет, то они определяют, куда его отправить по это таблице, делая вывод, что если сервер B недавно отправил пакет серверу А, то ответ от сервера А на этот же адрес и порт скорее всего надо переслать серверу B. А в отличии от TCP в UDP нет никаких договоренностей о поддержании сессии, т.к. нет и самого понятия сессии. WG же построен таким образом, что при отсуствии трафика, попадающего в туннель, не будет и трафика между пирами, только хедшейки раз в две минуты. Опция **PersistentKeepalive** заставлет его посылать пустые пакеты каждые 25 секунд, предовращая потерю маршрута на промежуточных роутерах, потому что иначе возможна ситуация, когда мы будем раз за разом отправить пакеты, а до второго пира они доходить не будут, а он об этом и не будет знать. Дальше мы для нашего клиента добавляем еще одну секцию peer в конфиг на internal: ```ini #notebook-client node [Peer] PublicKey = 26Vhud00ag/bdB9molvSxfBzZTlzdO+aZgrX3ZDncSg= AllowedIPs = 10.20.30.3/32 ``` Перезапускаем туннель на internal (```wg-quick down/up```), подключаемся.. Оп, хендшейк есть, данные пошли. Открываем какой-нибудь https://www.reg.ru/web-tools/myip, видим IP external ноды, и другую страну. Таким же образом создаем конфиги для других клиентов. Если это мобильные устройства, то удобнее показать им QR. Он делается следующим образом: создаем в текущей папке конфиг как обычно, конечно, с новыми ключами и другим IP, какой-нибудь ```wg-moblie-client.conf``` и дальше командой ```qrencode -t ansiutf8 < wg-moblie-client.conf``` показываем прям в консоли QR, который сканируем с телефона. Это удобнее копирования файлов, но вам так же никто не мешает скинуть ```wg-moblie-client.conf``` на телефон или вообще ввести значения 7 полей вручную. В целом, готово: мы только что сделали очень странный двуххоповый VPN. Желаем это отметить и заказать себе пива, открываем сбермаркет.. "СберМаркет не открывается. Если у вас работает VPN, отключите его". Ах да, мы же с этой проблемой и собирались бороться... Неловко. Давайте доделаем. ## Шаг четвертый: добавляем регион-зависимую маршрутизацию. Как мы помним, мы отправляем все данные с клиента на **internal**, а тот все данные отправляет на external, а тот уже своему провайдеру. Так же, мы помним, что у нас на **internal** "слабый" маршрут 0.0.0.0/0, который перебивается любыми другими маршрутами, а сам **internal** находится в российском сегменте. Значит, все, что нам надо — это как-то перехватить запросы на российские IP на уровне **internal** и перенаправить их не в туннель WG до **external**, а напрямую в сетевой порт самого сервера, в тот, через который он получает доступ в православный, российский интернет со скрепами и девицами в кокошниках. Давайте проверим предположение. На клиенте получим IP того же сбермаркета (```nslookup sbermarket.ru```), и посмотрим, как туда идет трафик (```traceroute 212.193.158.175```): ```bash HOST: vvzvladMBP14.local Loss% Snt Last Avg Best Wrst StDev 1.|-- 10.20.30.1 0.0% 10 3.9 4.3 3.2 6.5 1.1 2.|-- 10.20.30.2 0.0% 10 55.7 56.0 54.6 59.2 1.2 3.|-- 10.200.100.0 0.0% 10 55.5 56.1 54.9 58.6 1.1 4.|-- 10.197.37.65 0.0% 10 56.0 56.9 55.4 60.1 1.7 5.|-- 10.197.0.41 0.0% 10 56.1 57.0 55.7 60.9 1.6 ``` Ага, как и ожидалось, через **external**. Теперь создадим маршрут до этого адреса через дефолтный шлюз и устройство. Их можно узнать в ```ip r```: ```bash root@trikster-internal:~# ip r default via 195.2.79.1 dev ens3 onlink 10.20.30.2 dev wg-internal scope link ... ``` Вот 195.2.79.1 и ens3 и есть нужные нам данные. Используем уже знакомые нам подстановочные команды и создадим новый маршрут такой командой: ```bash target_ip="212.193.158.175/32" gateway=`ip route | awk '/default/ {print $3; exit}'` gateway_device=`ip route | awk '/default/ {print $5; exit}'` ip route add $target_ip via $gateway dev $gateway_device ``` Проверяем: ```bash root@trikster-internal:~# ip r default via 195.2.79.1 dev ens3 onlink 10.20.30.2 dev wg-internal scope link 10.20.30.3 dev wg-internal scope link 195.2.79.0/24 dev ens3 proto kernel scope link src 195.2.79.13 ==> 212.193.158.175 via 195.2.79.1 dev ens3 <== ``` Да, на последнем месте у нас нужный маршрут. Теперь повторяем команду ```traceroute -r 212.193.158.175``` на клиенте, и видим, что трейс другой: ```bash HOST: vvzvladMBP14.local Loss% Snt Last Avg Best Wrst StDev 1.|-- 10.20.30.1 0.0% 10 4.3 7.9 3.7 29.1 7.9 2.|-- host-89-22-232-243.hosted 0.0% 10 4.6 4.9 3.8 9.2 1.6 3.|-- 172.31.0.1 0.0% 10 25.9 8.4 3.3 25.9 6.9 4.|-- sw1-m9p2-msk.ip.ngenix.ne 0.0% 10 6.2 5.7 4.0 7.3 1.0 5.|-- cdn.ngenix.net 0.0% 10 3.8 5.0 3.8 8.4 1.3 ``` Сбермаркет, правда, все еще не открываемся: видимо, проверяет на наличие VPN какой-то другой сервер, а не тот, в адрес которого ресолвится имя домена. Можно сходить на https://asnlookup.com/, вбить туда адрес, и получить принадлежность адреса к AS и заодно список подсетей этой AS (AS34879, OOO Sovremennye setevye tekhnologii). С большой вероятностью для более-менее крупных компаний это и будет их сетевая инфраструктура (ну или по крайней мере, инфраструктура, относящаяся к конкретному сайту), прописав для которой маршруты, вы обеспечите доступ на нужный вам сайт/сервис. Для мелких сайтов вы скорее всего получите AS хостера или дата-центра, но, во-первых, это тоже сработает, а во-вторых, мелкие сайты обычно и не закрывают иностранные диапазоны, потому что не испытывают проблем с DDOSом из-за границы. Но можно сделать проще: засунуть в маршруты все адреса российского сегмента (спасибо [статье](https://habr.com/en/post/659655/) на хабре) и не париться о ручном добавлении. RIPE отдает их все в виде JSON вот по этому адресу: https://stat.ripe.net/data/country-resource-list/data.json?resource=ru Утилита jq преобразует из json в список подсетей: ```curl https://stat.ripe.net/data/country-resource-list/data.json?resource=ru | jq -r ".data.resources.ipv4[]"``` Правда, почему-то некоторые адреса там в формате ```195.85.234.0-195.85.236.255```, а не подсети, поэтому для них нам необходима еще утилита ipcalc: ```bash root@trikster-internal:~# ipcalc 195.85.234.0-195.85.236.255 |grep -v "deaggregate" 195.85.234.0/23 195.85.236.0/24 ``` Выделить эти адреса из базового списка можно банально через ```grep '-'``` или ```grep -v '/'```. Но их там немного, и на них, в принципе, можно забить. Скрипт на баше выглядит как-то так (я не удержался и добавил туда еще и прогрессбар): ```bash #!/bin/bash function ProgressBar { let _progress=(${1}*100/${2}*100)/100 let _done=(${_progress}*4)/10 let _left=40-$_done _fill=$(printf "%${_done}s") _empty=$(printf "%${_left}s") printf "\rAddind routes : [${_fill// /#}${_empty// /-}] ${_progress}%%" } #Variables file_raw="russian_subnets_list_raw.txt" file_user="subnets_user_list.txt" file_for_calc="russian_subnets_list_raw_for_calc.txt" file_processed="russian_subnets_list_processed.txt" gateway_for_internal_ip=`ip route | awk '/default/ {print $3; exit}'` interface=`ip link show | awk -F ': ' '/state UP/ {print $2}'` #Get addresses RU segment echo "Download RU subnets..." curl --progress-bar "https://stat.ripe.net/data/country-resource-list/data.json?resource=ru" | jq -r ".data.resources.ipv4[]" > $file_raw echo "Deaggregate subnets..." cat $file_raw |grep "-" > $file_for_calc cat $file_raw |grep -v "-" > $file_processed for line in $(cat $file_for_calc); do ipcalc $line |grep -v "deaggregate" >> $file_processed; done if [ -e $file_user ]; then echo "Add user subnets..."; cat $file_user >> $file_processed; fi #Flush route table echo "Flush route table (down and up interface)..." ifdown $interface && ifup $interface #Add route routes_count_in_file=`wc -l $file_processed` routes_count_current=0 for line in $(cat $file_processed); do ip route add $line via $gateway_for_internal_ip dev $interface; let "routes_count_current+=1" ; ProgressBar ${routes_count_current} ${routes_count_in_file}; done echo "" echo "Remove temp files..." rm $file_raw $file_processed $file_json $file_for_calc routes_count=`ip r | wc -l` echo "Routes in routing table: $routes_count" ``` Добавим строчки в крон (```export EDITOR=nano; crontab -e```), чтобы он запускался каждую неделю (для того, чтобы обновить список адресов, если они поменялись) и после перезагрузки: ``` @reboot sleep 30 && bash /root/update_ru_routes.sh > /root/update_routes_log.txt 2>&1 0 3 * * mon bash /root/update_ru_routes.sh > /root/update_routes_log.txt 2>&1 ``` Если вам принудительно надо маршрутизовать какую-то сеть через **internal**, то можно рядом со скриптом создать файлик ```subnets_user_list.txt``` в который поместить список подсетей, тогда они каждый раз будут добавляться к общему списку при обновлении, в bash-скрипте это есть. ## Шаг пятый: настраиваем фаервол Для начала на обоих серверах редактируем файл ```/etc/default/ufw```, изменяя значение "**DEFAULT_FORWARD_POLICY**" на **ACCEPT**. Теперь выполняем следующие команды на **internal**: ```bash ufw reset ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 17968/udp ufw allow in on wg-internal systemctl enable ufw --now ufw enable ``` Что происходит, думаю, понятно — запретить все, разрешить исходящие, входящие ssh и подключения к WG, а что приходит из туннеля — разрешить. На **external** тоже самое, но открывать порт для WG не надо — он подключается сам. ```bash ufw reset ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow in on wg-external systemctl enable ufw --now ufw enable ``` Еще хорошо бы поставить и настроить fail2ban, или хотя бы перенести ssh на другой порт. В любом случае, отключение парольной авторизации на ssh вообще и переход только на ключи — это базовая операция. ## Шаг шестой, бонусный и необязательный: кеширующий защищенный DNS over HTTPS Теперь нам нужна еще одна вещь: DNS. Можно, конечно, жить с DNS 1.1.1.1, но надо учитывать две вещи: Трафик на него пойдет через **external**, что автоматически означает задержку порядка 100мс при каждом запросе. Можно, конечно, добавить 1.1.1.1/32 в ```subnets_user_list.txt```, и тогда трафик пойдет через локальную ноду и локальный сервер 1.1.1.1, что уменьшит задержку до 10-20мс, но ваши DNS-запросы будет доступны вашему провайдеру, что в случае локальной ноды может быть для кого-то неприемлимо. Несколькими командами можно легко сделать кеширующий DNS, который еще и будет работать с DNS over HTTPS, а значит, провайдеру будет доступен только сам факт использовани DoH, но не сами запросы. Но это, конечно, не обязательно: у меня **internal** находится в домашней сети, и я просто использую DNS микротика, который находится в той же сети. Но если у вас **internal** сервер это VPS, то можно сделать там и DNS сервер. Использовать будем ```cloudflared```. Добавляем репозитарий: ```bash mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared buster main' | tee /etc/apt/sources.list.d/cloudflared.list sudo apt-get update && sudo apt-get install cloudflared dnsmasq -y ``` Пишем конфиг: ```/etc/cloudflared/config.yml```: ```yml logfile: /var/log/cloudflared.log proxy-dns: true proxy-dns-upstream: - https://1.0.0.1/dns-query - https://1.1.1.1/dns-query - https://2606:4700:4700::1111/dns-query - https://2606:4700:4700::1001/dns-query proxy-dns-port: 5353 proxy-dns-address: 127.0.0.1 ``` Либо, можете заменить в proxy-dns-upstream записи на https://security.cloudflare-dns.com/dns-query или https://9.9.9.9/dns-query — первое это блокировка всякой малвари от cloudflare, второе это Quad9. Создаем сервис: ```/etc/systemd/system/cloudflared.service``` ```ini [Unit] Description=DNS over HTTPS (DoH) proxy client Wants=network-online.target nss-lookup.target Before=nss-lookup.target [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE DynamicUser=yes ExecStart=/usr/local/bin/cloudflared --config /etc/cloudflared/config.yml [Install] WantedBy=multi-user.target ``` Активируем и запускаем ```bash systemctl daemon-reload systemctl enable cloudflared systemctl start cloudflared systemctl status cloudflared ``` Проверяем: ```dig @127.0.0.1 -p5353 google.com``` У cloudflared есть один минус — у него странное кеширование, которое держится совсем немного, поэтому для кеширования дополнительно настроим dnsmasq, указав ему в качестве сервера cloudflared. Добавляем в ```/etc/dnsmasq.conf``` что-то вроде ```ini server=127.0.0.1#5353 no-poll no-resolv listen-address=10.20.30.1, 127.0.0.1 cache-size=1500 stop-dns-rebind clear-on-reload no-negcache ``` Запускаем: ```systemctl restart dnsmasq.service``` Проверяем: ```dig @127.0.0.1 google.com``` И уже можно сделать это с клиента: ```dig @10.20.30.1 google.com``` Если все ок, то можно проверить несколько раз — при повторных запросах Query time: должен стать 0 msec или около, если запрашиваете локалхост, или будет близко к пингу до **internal**, если делаете это с клиента. Теперь можно добавить в конфиги клиентов в секцию **Interface**: ```ini DNS = 10.20.30.1, 1.1.1.1 ``` P.S. особые параноики могут запустить cloudflared на **external**, и скрыть от локального провайдера даже сам факт использования DoH. Для этого в proxy-dns-address в конфиге cloudflared и в dnsmasq.conf надо просто указать 10.20.30.2. Кстати, в качестве альтернативы можно поставить рядом на сервер [pi-hole](https://github.com/pi-hole/pi-hole/), который делает примерно тоже самое, но еще блокирует рекламу и показывает красивую статистику. Пары ключей в статье — действующие, так что вы можете, ничего не исправляя в конфигах (только IP и имена адаптеров), залить их на два своих сервера и клиент и поиграться. Но для боевого применения ключи надо перегенерить, конечно. Если кто-то захочет это все красиво обернуть в два докера и прикрутить к этому веб-интерфейс (потому что конфиги клиентов все же удобнее создавать в нем) — добро пожаловать в issues на гитхабе.