За­бав­ная ситу­ация: сай­тов и сер­висов, дос­тупных толь­ко через VPN, все боль­ше, но при этом мно­гие рос­сий­ские ком­пании зак­рыва­ют дос­туп из‑за гра­ницы. В резуль­тате при­ходит­ся целыми дня­ми теребить пол­зунки «вкл‑выкл», что уто­митель­но. Я рас­ска­жу, как с помощью магии мар­шру­тов и WireGuard, решить эту проб­лему и сде­лать «умный» VPN, который не надо отклю­чать.

Ес­ли ты поль­зуешь­ся VPN, то и сам навер­няка стал­кива­ешь­ся с бло­киров­ками зарубеж­ного тра­фика. К при­меру, могут не откры­вать­ся pochta.ru, leroymerlin.ru, rt.ru, avito.ru.

По­луча­ется мем.

Каж­дый с этим борет­ся как может. Нап­ример, на устрой­ствах Apple род­ными средс­тва­ми мож­но нас­тро­ить авто­мати­зацию, которая будет запус­кать VPN, ког­да откры­ваешь опре­делен­ные при­ложе­ния (нап­ример, Twitter), а ког­да выходишь из них — вык­лючать обратно. Но это кос­тыль, а хочет­ся все сде­лать кра­сиво, да еще и про­качать навык работы с сетью.

По­это­му мы сей­час поп­робу­ем «вклю­чать чуть‑чуть VPN».

info

При таком исполь­зовании это ско­рее прок­си, а не VPN, но любые методы обхо­да бло­киро­вок теперь называ­ют VPN, так что, сде­лав эту ого­вор­ку, даль­ше рас­сла­бим­ся и будем делать как все.

За­одно чуть улуч­шим качес­тво свя­зи с локаль­ными ресур­сами: необ­ходимость тас­кать тра­фик сна­чала до VPN вне стра­ны, а потом обратно до сер­вера внут­ри нее дра­матич­но ска­зыва­ется если не на ско­рос­ти, то на задер­жке точ­но. Даже на про­вод­ном интерне­те пинг в 4 мс до Яндекса лег­ко прев­раща­ется в 190 мс, а на мобиль­ном интерне­те — из 80 мс в 240 мс. Допол­нитель­ный хоп внут­ри стра­ны чуть ухуд­шит дело, но далеко не так дра­матич­но.

Де­лать все мы будем на осно­ве WireGuard — это отно­ситель­но новая (раз­рабаты­вает­ся с 2016 года, в отли­чие от OpenVPN и IPsec: пер­вый — это двух­тысяч­ные, а вто­рой еще рань­ше) тех­нология VPN. Соз­дал ее, по сути, один человек — zx2c4, которо­го в миру зовут Джей­соном Донен­фель­дом. Плю­сы WG — ско­рость (осо­бен­но для Linux, где он может работать как модуль ядра, начиная с Kernel 5.6, и Windows, где модуль для ядра выпус­тили око­ло недели назад), низ­кие задер­жки, сов­ремен­ная крип­тогра­фия и прос­тая нас­трой­ка и исполь­зование конеч­ным юзе­ром.

Ах да, еще UDP. UDP для тун­нелей — это хорошо, потому что у TCP уже есть механиз­мы, которые поз­воля­ют ему работать на неидеаль­ных соеди­нени­ях, а UDP пред­став­ляет собой имен­но такое соеди­нение. А ког­да ты засовы­ваешь TCP в TCP, то отка­зыва­ешь­ся от боль­шей час­ти этих механиз­мов (инкапсу­лиро­ван­ный TCP-пакет будет гаран­тирован­но дос­тавлен дру­гой сто­роне, хотя про­токол допус­кает недос­тавку), но все еще несешь весь овер­хед вида «хен­дшейк соеди­нения для отправ­ки хен­дшей­ка».

Не говоря уж о том, что инкапсу­лиро­вать UDP в TCP — ничуть не луч­шая идея, потому что сра­зу рушит все пред­положе­ния вся­ких скай­пов о том, что луч­ше про­пус­тить пару пакетов, чем умень­шить задер­жку: каж­дый UDP-пакет в этом слу­чае будет при­нуди­тель­но пос­лан заново и дос­тавлен кор­рек­тно, не счи­таясь с зат­ратами вре­мени.

Осо­бен­но для оди­ноко­го поль­зовате­ля‑хакера при­ятна работа с шиф­ровани­ем: нет необ­ходимос­ти ни в сер­тифика­тах и удос­товеря­ющих цен­трах, ни в логинах‑паролях, все, что нуж­но, — обме­нять­ся пуб­личны­ми клю­чами с пиром, с которым ты хочешь уста­новить соеди­нение. Для боль­ших ком­паний это, конеч­но, будет ско­рее минусом, как и то, что WG — это толь­ко базовая часть пол­ноцен­ной боль­шой инфраструк­туры VPN. Но имен­но WireGuard исполь­зовали, к при­меру, в Cloudflare для сво­его WARP, прав­да, написав его собс­твен­ную реали­зацию — boringtun.

Еще один минус WG — то, что тра­фик не обфусци­рован и глу­бокая инспек­ция пакетов (DPI) выявит и поз­волит заб­локиро­вать такое соеди­нение (не говоря уж о бло­киров­ке UDP сов­сем, что поч­ти не меша­ет работать с вебом, но гаран­тирован­но лома­ет WireGuard). Для скры­тия тра­фика рекомен­дует­ся исполь­зовать спе­циали­зиро­ван­ное ПО — Cloak, Obfsproxy, Shadowsocks, Stunnel, SoftEther, SSTP или, в кон­це кон­цов, прос­той SSH. Часть из этих инс­тру­мен­тов может работать сов­мес­тно с WG, а часть спо­соб­на его заменять в качес­тве инс­тру­мен­та сте­ганог­рафии: WG изна­чаль­но соз­давал­ся под ско­рость и крип­тогра­фичес­кую защищен­ность.

Ес­ли очень упро­щать, клю­чи работа­ют сле­дующим обра­зом: у нас есть зак­рытый (при­ват­ный) ключ, из которо­го мож­но сге­нери­ровать откры­тый, или пуб­личный. Наобо­рот, из откры­того клю­ча получить зак­рытый мы никак не можем. Затем мы шиф­руем с помощью зак­рытого клю­ча какую‑то стро­ку, а при помощи откры­того рас­шифру­ем ее и тем самым убе­дим­ся, что у собесед­ника точ­но есть зак­рытый ключ, а зна­чит, он тот, за кого себя выда­ет. Таким обра­зом, мы можем без проб­лем переда­вать откры­тый ключ — он все­го лишь поз­воля­ет про­верить под­линность авто­ра, но не прит­ворить­ся им.

Это как в SSH — пуб­личный ключ лежит на сер­вере, где его потеря — неболь­шая беда: все, что смо­жет сде­лать с ним зло­умыш­ленник, — это положить его на свой сер­вер, что­бы ты мог под­клю­чить­ся к нему с помощью зак­рытого клю­ча.

Так вот, в WG на пер­вом эта­пе под­клю­чения каж­дая сто­рона с помощью зашиф­рован­ного при­ват­ным клю­чом сооб­щения доказы­вает собесед­нику, что она имен­но она: это про­веря­ется пуб­личным клю­чом.

Вто­рой этап — соз­дание с помощью этих клю­чей и матана сим­метрич­ных клю­чей для шиф­рования самого тра­фика. Бла­года­ря тому что рас­шифро­вать зашиф­рован­ное пуб­личным клю­чом нель­зя без при­ват­ного, мы смо­жем соз­дать ключ для сим­метрич­ного шиф­рования и отпра­вить его по защищен­ному каналу. Этот шаг необ­ходим потому, что сим­метрич­ное шиф­рование гораз­до менее ресур­соем­кая опе­рация, а минус у нее толь­ко один: необ­ходимо син­хро­низи­ровать ключ меж­ду сто­рона­ми, при том что перех­ват клю­ча треть­ей сто­роной ведет к воз­можнос­ти рас­шифров­ки тра­фика.

Но эта проб­лема реша­ется с помощью асим­метрич­ной схе­мы. Это называ­ется про­токол Диф­фи — Хел­лма­на — спо­соб защищен­ного получе­ния обще­го сек­ретно­го клю­ча. В WG исполь­зует­ся ECDH — вари­ация Диф­фи — Хел­лма­на на эллипти­чес­ких кри­вых. Пер­вые два эта­па в тер­минах WG называ­ются рукопо­жати­ем или хен­дшей­ком.
За­тем сим­метрич­ные клю­чи исполь­зуют­ся для шиф­рования тра­фика. Раз в две минуты про­исхо­дит новое рукопо­жатие и сес­сион­ные клю­чи меня­ются.

Ра­зуме­ется, в реаль­нос­ти все нем­ного слож­нее: нап­ример, отправ­ляют­ся не сами клю­чи, а сге­нери­рован­ные на их осно­ве эфе­мер­ные клю­чи, которые уда­ляют­ся сра­зу пос­ле опе­рации. Заин­тересо­вав­шихся под­робнос­тями отправ­ляю к крат­кому опи­санию на сай­те WireGuard.

Мы же перей­дем к более прак­тичес­ким дей­стви­ям.

 

Шаг 1. Создаем и настраиваем два сервера

Один сер­вер будет внут­ри стра­ны — через него тра­фик пой­дет на локаль­ные ресур­сы, а вто­рой — за гра­ницей. Даль­ше я их буду называть local и external.

Иде­аль­но, если local будет в тво­ей домаш­ней сети, потому что при этом тра­фик на внеш­ние ресур­сы не отли­чает­ся от тво­его домаш­него тра­фика. Но для это­го нужен какой‑то хост дома, белый IP и воз­можность проб­росить порт. У меня это вир­туал­ка на домаш­нем сер­вере, но, навер­ное, подой­дет и Raspberry Pi или ана­логич­ный одноплат­ник.

info

Ва­риант с одноплат­ником я не тес­тировал и не уве­рен, что он сра­бота­ет. Raspberry Pi при­дет­ся мар­шру­тизи­ровать весь тра­фик с устрой­ств и дер­жать в памяти око­ло 11 тысяч мар­шру­тов; ресур­сов на это может не хва­тить.

Ес­ли дома хос­та нет, мож­но взять любой сер­вер у хос­тера VDS. Увы, некото­рые сай­ты бло­киру­ют под­сети хос­теров, опа­саясь ботов, так что это менее пред­почти­тель­ный вари­ант.

А вот внеш­нюю машину мож­но арен­довать и у хос­тера. К при­меру, у RU VDS и VDSina есть зарубеж­ные пло­щад­ки. А мож­но выб­рать инос­тран­ного хос­тера, если най­дешь спо­соб опла­чивать его услу­ги. Нап­ример, я исполь­зую исланд­ский 1984.hosting, соз­данный спе­циаль­но для парано­иков.

Счи­таем, что на обо­их сер­верах у нас Debian 11.

Ста­вим нуж­ные нам пакеты:

apt update && apt install -y wireguard iptables ipcalc qrencode curl jq traceroute dnsutils ufw

Вклю­чаем перенап­равле­ние тра­фика: в этом слу­чае сер­вер, получив пакет, который не пред­назна­чает­ся ни одно­му из его IP-адре­сов, не отбро­сит его, а попыта­ется перенап­равить в соот­ветс­твии со сво­ими мар­шру­тами.

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 обо­их сер­веров, что­бы не запутать­ся, где какая кон­соль:

hostnamectl set-hostname trickster-internal
hostnamectl set-hostname trickster-external
 

Шаг 2. Настраиваем WireGuard для связи двух серверов

Для начала генери­руем клю­чи. Запус­каем два раза wg genkey и получа­ем два при­ват­ных клю­ча:

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
[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 addr show $(ip route | awk '/default/ { print $5 }') | grep "inet" | grep -v "inet6" | head -n 1 | awk '/inet/ {print $2}' | awk -F/ '{print $1}'` table main
PostDown = iptables -t nat -D POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE
PostDown = ip rule del from `ip addr show $(ip route | awk '/default/ { print $5 }') | grep "inet" | grep -v "inet6" | head -n 1 | awk '/inet/ {print $2}' | awk -F/ '{print $1}'` table main

Вто­рой на external:

/etc/wireguard/wg-external.conf
[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

info

Ути­лита wg-quick — это на самом деле 400 строк на баше, которые авто­мати­зиру­ют час­то исполь­зуемые вещи, нап­ример уста­нов­ку мар­шру­тов. Наличие тун­неля само по себе не дает ничего, кро­ме защищен­ной «тру­бы», за которой находит­ся дру­гой пир. Что­бы твой зап­рос в бра­узе­ре попал в интерфейс, сис­теме надо явно ска­зать: «Мар­шру­тизи­руй, пожалуй­ста, пакеты с таким‑то адре­сом наз­начения вот в этот сетевой интерфейс».

Имен­но этим занима­ется wg-quick. Ну еще и нас­трой­кой адре­сов DNS, ука­зан­ных в кон­фиге, уста­нов­кой MTU и еще парой вещей. Но ничего слож­ного в этом нет, дос­таточ­но сде­лать cat /usr/bin/wg-quick, что­бы пос­мотреть на эту логику, и, если надо, пов­торить то же самое руками.

  • Interface-Address — это IP текуще­го пира. Вся адре­сация в WG ста­тичес­кая. С одной сто­роны, это упро­щает нас­трой­ку и бутс­трап, с дру­гой сто­роны, усложня­ет работу, если у тебя очень мно­го кли­ентов.
  • ListenPort — это UDP-порт для под­клю­чения извне. Если не ука­зать, будет прос­лушивать 51820.
  • Interface-PostUp и Interface-PostDown — скрип­ты, которые выпол­няют­ся пос­ле под­нятия и пос­ле оста­нов­ки интерфей­са. Есть еще PreUP и PreDown.

Кро­ме пуб­личных и при­ват­ных клю­чей, есть еще опция PresharedKey, которая обес­печива­ет допол­нитель­ное шиф­рование сим­метрич­ным шиф­ром. Ключ мож­но сге­нери­ровать, нап­ример, коман­дой wg genpsk и добавить в опцию PresharedKey в сек­циях Peer на обо­их пирах. Если не исполь­зовать эту опцию, наг­рузка по шиф­рованию и рас­шифров­ке не вырас­тет: ког­да ключ не ука­зан, исполь­зует­ся нулевое зна­чение клю­ча.

А что­бы по‑нас­тояще­му обес­печить пост­кван­товую безопас­ность (невоз­можность рас­шифров­ки дан­ных кван­товыми компь­юте­рами), раз­работ­чики рекомен­дуют допол­нитель­ный внеш­ний кван­тово‑устой­чивый механизм хен­дшей­ка, нап­ример SIDH, который Microsoft пиарит имен­но в таком кон­тек­сте. Соз­данный им общий ключ мож­но исполь­зовать в качес­тве PresharedKey.

Зак­линания в PostUp дос­таточ­но прос­ты. Вот коман­да для под­ста­нов­ки име­ни сетево­го интерфей­са, куда по умол­чанию выпол­няет­ся мар­шру­тиза­ция:

ip route | awk '/default/ {print $5; exit}'

Как пра­вило, это интерфейс, обра­щен­ный к про­вай­деру или роуте­ру.

Та­ким обра­зом, страш­ная коман­да прев­раща­ется в такую:

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

Здесь про­исхо­дит вклю­чение NAT в режиме мас­карада: сер­вер будет отправ­лять при­шед­шие ему пакеты во внеш­нюю сеть, под­меняя адрес отпра­вите­ля сво­им, что­бы отве­ты на эти пакеты тоже при­ходи­ли ему, а не исходно­му отпра­вите­лю.

Вто­рая коман­да уже нем­ного слож­нее, но она под­став­ляет IP-адрес дефол­тно­го мар­шру­та.

`ip addr show $(ip route | awk '/default/ { print $5 }') | grep "inet" | grep -v "inet6" | head -n 1 | awk '/inet/ {print $2}' | awk -F/ '{print $1}'`

Сна­чала мы получа­ем, как и выше, сетевой интерфейс мар­шру­та по умол­чанию:

root@:~# ip route | awk '/default/ { print $5 }'
enp1s0

По­том дан­ные о сос­тоянии это­го интерфей­са:

root@:~# ip route | awk '/default/ { print $5 }'
inet 192.168.88.70/24 brd 192.168.88.255 scope global dynamic enp1s0

И даль­ше вытас­кива­ем отту­да адрес, в дан­ном слу­чае 192.168.88.70.
Ко­ман­да ста­новит­ся такой:

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 — это стре­лять из пуш­ки по воробь­ям, а вот допол­нитель­ное пра­вило исправ­ляет ситу­ацию.

info

Я намерен­но не при­вожу готовые кон­фиги, потому что хочу показать механизм соз­дания кон­фигов в руч­ном режиме. В свое вре­мя я генери­ровал кон­фиги ути­лита­ми типа easy-wg-quick или веб‑сер­висами, которые спра­шива­ют тебя о наз­вании кли­ента и кра­сиво показы­вают QR-код. Это отнюдь не спо­собс­тву­ет понима­нию того, как работа­ет WG на самом деле, и может выз­вать проб­лемы.

Те­перь в оба кон­фига надо добавить сек­цию Peer, что­бы свя­зать сер­веры друг с дру­гом.

Ге­нери­руем из при­ват­ного клю­ча пуб­личный (вот в wg pubkey как раз и про­исхо­дит крип­томагия):

root@:~# echo "kOd3FVBggwpjD3AlZKXUxNTzJT0+f3MJdUdR8n6ZBn8=" | wg pubkey
MxnOnIlKfSyZyRutnYyoWHb3Izjalgf1t8F1oPJiyyw=

Это пуб­личный ключ сер­вера internal, его мы помеща­ем в сек­цию Peer на external:

/etc/wireguard/wg-external.conf
[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:

root@:~# echo "6CCRP42JiTObyf64Vo0BcqsX6vptsqOU+MKUslUun28=" | wg pubkey
FulnUTovyyfgn5kmgPkcj2OjKRFGeLkaTsHtAOy6HW8=

Мы получа­ем пуб­личный ключ сер­вера external и помеща­ем его в сек­цию Peer сер­вера internal.

/etc/wireguard/wg-internal.conf
[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
[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 addr show $(ip route | awk '/default/ { print $5 }') | grep "inet" | grep -v "inet6" | head -n 1 | awk '/inet/ {print $2}' | awk -F/ '{print $1}'` table main
PostDown = iptables -t nat -D POSTROUTING -o `ip route | awk '/default/ {print $5; exit}'` -j MASQUERADE
PostDown = ip rule del from `ip addr show $(ip route | awk '/default/ { print $5 }') | grep "inet" | grep -v "inet6" | head -n 1 | awk '/inet/ {print $2}' | awk -F/ '{print $1}'` table main
#external node
[Peer]
PublicKey = FulnUTovyyfgn5kmgPkcj2OjKRFGeLkaTsHtAOy6HW8=
AllowedIPs = 10.20.30.2/32, 0.0.0.0/0
/etc/wireguard/wg-external.conf
[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

Те­перь мож­но под­нять тун­нели на обо­их сер­верах:

root@trikster-external:~# wg-quick down wg-external ; wg-quick up wg-external
root@trikster-internal:~# wg-quick down wg-internal ; wg-quick up wg-internal

Про­веря­ем, что тун­нели активны, коман­дой wg:

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: ... seconds ago» и бай­ты и в received и в sent, зна­чит, все хорошо. Если бай­ты толь­ко в send, без хен­дшей­ка и получен­ных дан­ных, зна­чит, где‑то в кон­фиге ошиб­ка или сер­веры недос­тупны друг для дру­га.

Ес­ли что‑то пош­ло не так и отва­лил­ся SSH, то дос­таточ­но перезаг­рузить сер­вер — активные тун­нели сбро­сят­ся.

Ес­ли же все хорошо и дос­туп к сер­верам сох­ранил­ся, ста­вим тун­нели в авто­запуск:

root@trikster-internal:~# systemctl enable wg-quick@wg-internal.service
root@trikster-external:~# systemctl enable wg-quick@wg-external.service

Поп­робу­ем пос­мотреть мар­шрут (рекомен­дую замеча­тель­ную ути­литу mytraceroute, mtr) без тун­неля:

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

И с тун­нелем:

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, который у нас наз­начен выход­ной нодой, а потом через его мар­шру­тиза­торы.

У нас получи­лась при­мер­но сле­дующая схе­ма.

 

Шаг 3. Добавляем конфиг клиента

Соз­даем кон­фиг кли­ента, конеч­ного устрой­ства — поль­зовате­ля VPN. За осно­ву берем wg-external.conf, потому что это точ­но такой же кли­ент, который под­клю­чает­ся к internal: раз­ница толь­ко в том, что external будет получать пакеты, а наш кли­ент — отправ­лять.

Ге­нери­руем ему сра­зу пару пуб­личный и при­ват­ный ключ:

root@:~# 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
[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.

В UDP, в отли­чие от TCP, нет никаких догово­рен­ностей о под­держа­нии сес­сии, так как нет и самого понятия сес­сии. WG же пос­тро­ен таким обра­зом, что при отсутс­твии тра­фика, попада­юще­го в тун­нель, не будет и тра­фика меж­ду пирами, толь­ко хен­дшей­ки раз в две минуты. Опция PersistentKeepalive зас­тавля­ет его посылать пус­тые пакеты каж­дые 25 с, что пре­дот­вра­щает потерю мар­шру­та на про­межу­точ­ных роуте­рах. Без это­го мы бы мог­ли раз за разом отправ­лять пакеты, до вто­рого пира они не доходи­ли бы, а он бы об этом не знал.

Даль­ше мы добав­ляем еще одну сек­цию Peer в кон­фиг на internal — для кли­ента:

#notebook-client node
[Peer]
PublicKey = 26Vhud00ag/bdB9molvSxfBzZTlzdO+aZgrX3ZDncSg=
AllowedIPs = 10.20.30.3/32

Пе­реза­пус­каем тун­нель на internal (wg-quick down/up), под­клю­чаем­ся… Есть хен­дшейк! Дан­ные пош­ли.

Те­перь смот­рим свой IP (нап­ример, на reg.ru): видим IP external ноды и дру­гую стра­ну.

Та­ким же обра­зом соз­даем кон­фиги для дру­гих кли­ентов. Если это мобиль­ные устрой­ства, то удоб­нее показать им QR-код. Он дела­ется сле­дующим обра­зом. Соз­даем в текущей пап­ке кон­фиг как обыч­но (конеч­но, с новыми клю­чами и дру­гим IP), называ­ем, нап­ример, wg-moblie-client.conf. А даль­ше пря­мо коман­дой в кон­соли соз­даем QR-код, который ска­ниру­ем с телефо­на:

root@:~# qrencode -t ansiutf8 < wg-moblie-client.conf

Это удоб­нее, чем копиро­вание фай­лов, но тебе ник­то не меша­ет ски­нуть wg-moblie-client.conf на телефон или вооб­ще ввес­ти зна­чения семи полей вруч­ную.

Те­перь наша схе­ма выг­лядит сле­дующим обра­зом.

В целом все готово: мы толь­ко что сде­лали очень стран­ный двух­хоповый VPN. Надо это дело отме­тить! Откры­ваем Сбер­маркет, что­бы заказать пив­ка...

Ах да, мы же с этой проб­лемой и собира­лись бороть­ся. Нелов­ко!
Да­вай додела­ем.

 

Шаг 4. Добавляем регионально зависимую маршрутизацию

Как ты пом­нишь, мы отправ­ляем все дан­ные с кли­ента на internal, он все дан­ные шлет на external, а тот уже сво­ему про­вай­деру. Так­же мы пом­ним, что у нас на internal «сла­бый» мар­шрут 0.0.0.0/0, который переби­вает­ся любыми дру­гими мар­шру­тами, а сам internal находит­ся в рос­сий­ском сег­менте. Зна­чит, все, что нам надо, — это как‑то перех­ватить зап­росы на рос­сий­ские IP на уров­не internal и перенап­равить их не в тун­нель WG до external, а нап­рямую в сетевой порт самого сер­вера, в тот, через который он получа­ет дос­туп в пра­вос­лавный, рос­сий­ский интернет со скре­пами и девица­ми в кокош­никах.

Да­вай про­верим пред­положе­ние. На кли­енте получим IP того же Сбер­марке­та (nslookup sbermarket.ru) и пос­мотрим, как туда идет тра­фик (traceroute 212.193.158.175):

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:

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 — это и есть нуж­ные нам дан­ные. Исполь­зуем уже зна­комые под­ста­новоч­ные коман­ды и соз­дадим новый мар­шрут:

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

Про­веря­ем:

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 на кли­енте и видим, что трейс дру­гой:

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

Ура, Сбер­маркет откры­вает­ся! Прав­да, такое сра­бота­ет не со все­ми сер­висами: нап­ример, JavaScript на сай­те может дер­нуть дру­гой сер­вер, а не тот, в адрес которо­го резол­вится имя домена и до которо­го нет мар­шру­та.

Мож­но схо­дить на asnlookup.com, вбить туда адрес и получить при­над­лежность адре­са к AS и заод­но спи­сок под­сетей этой autonomous system (у Сбер­марке­та это AS34879, OOO Sovremennye setevye tekhnologii). С боль­шой веро­ятностью для более‑менее круп­ных ком­паний это и будет их сетевая инфраструк­тура (ну или, по край­ней мере, инфраструк­тура, отно­сяща­яся к кон­крет­ному сай­ту), про­писав для которой мар­шру­ты ты обес­печишь дос­туп на нуж­ный сайт или сер­вис. Для мел­ких сай­тов ты, ско­рее все­го, получишь AS хос­тера или дата‑цен­тра, но, во‑пер­вых, это тоже сра­бота­ет, а во‑вто­рых, мел­кие сай­ты обыч­но и не зак­рыва­ют инос­тран­ные диапа­зоны, потому что не испы­тыва­ют проб­лем с DDoS из‑за гра­ницы.

Но мож­но сде­лать про­ще и авто­мати­чес­ки: засунуть в мар­шру­ты вооб­ще все адре­са рос­сий­ско­го сег­мента (спа­сибо статье на Хаб­ре) и не парить­ся о руч­ном добав­лении.

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.

root@:~# 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 '/'.

Скрипт для заг­рузки роутов выг­лядит как‑то так (я не удер­жался и добавил туда еще и прог­ресс‑бар):

#!/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 "\rAdd routes to route table (${1}/${2}): [${_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 |grep -v "#" >> $file_processed; fi
# Flush route table
echo "Flush route table (down interface $interface)..."
ifdown $interface > /dev/null 2>&1
echo "Up interface $interface..."
ifup $interface > /dev/null 2>&1
# 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"

До­бавим строч­ки в крон (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, в который помес­тить спи­сок под­сетей, тог­да они каж­дый раз будут добав­лять­ся к обще­му спис­ку при обновле­нии (в скрип­те выше эта воз­можность уже реали­зова­на).

Мой, нап­ример, выг­лядит так:

#avito
146.158.48.0/21
#
#telegram
91.108.4.0/22
91.108.8.0/22
91.108.58.0/23
95.161.64.0/20
149.154.160.0/21

Пер­вая под­сеть — для мобиль­ного при­ложе­ния «Ави­то»: ее почему‑то не было в спис­ке RIPE. Даль­ше под­сети для «Телег­рама», что­бы хоть нем­ного уско­рить заг­рузку фото и видео.

Про­веря­ем.

Как видишь, раз­ные сер­висы показы­вают нам раз­ные адре­са, потому что один сер­вис хос­тится где‑то внут­ри Рос­сии, а дру­гой — сна­ружи. Работа­ет!

Кста­ти, если у тебя internal находит­ся в домаш­ней сети, бонусом ты получишь дос­туп к домаш­ней сети из любого мес­та, где находит­ся устрой­ство со вклю­чен­ным VPN: мар­шрут 0.0.0.0/0 на устрой­стве отправ­ляет в VPN весь тра­фик, а internal, замечая тра­фик в ту под­сеть, в которой он находит­ся, отправ­ляет ее в локаль­ный порт, а не в тун­нель до external. Очень удоб­но: у меня в домаш­ней сети работа­ет сер­вер с докер‑кон­тей­нерами web2rss, ownCloud, navidrome, freshrss, rss-bridge, homeassistant, и мне для получе­ния дос­тупа к ним совер­шенно не надо замора­чивать­ся с проб­росом пор­тов, авто­риза­цией каж­дого сер­вера и HTTPS, не говоря уж о том, что некото­рые сер­висы, типа IoT-устрой­ств, не име­ют ни авто­риза­ции, ни шиф­рования в прин­ципе.

Окончательная схема
Окон­чатель­ная схе­ма
 

Шаг 5. Настраиваем файрвол

По­лез­ной при­выч­кой и хорошим тоном будет зак­рыть все ненуж­ное на сер­верах.

Для начала на обо­их сер­верах редак­тиру­ем файл /etc/default/ufw, изме­няя зна­чение DEFAULT_FORWARD_POLICY на ACCEPT.

Те­перь выпол­няем сле­дующие коман­ды на internal:

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 не надо — он под­клю­чает­ся сам.

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 вооб­ще и переход толь­ко на авто­риза­цию по клю­чу — это базовая опе­рация, не зависи­мо ни от чего.

 

Шаг 6, бонусный и необязательный. Кеширующий защищенный 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 «Мик­ротика», который находит­ся в той же сети и под­держи­вает DoH. Но если у тебя internal-сер­вер — это VPS, то мож­но сде­лать там и DNS-сер­вер. Исполь­зовать будем cloudflared и dnsmasq.

До­бав­ляй репози­тарий:

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
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

Пер­вый — это бло­киров­щик мал­вари, вто­рой — это Quad9.

Соз­дай сер­вис:

/etc/systemd/system/cloudflared.service
[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

Те­перь оста­лось запус­тить его и засунуть в авто­заг­рузку:

systemctl daemon-reload
systemctl start cloudflared
systemctl status cloudflared
systemctl enable cloudflared

Про­веря­ем:

dig @127.0.0.1 -p5353 google.com

У cloudflared есть один минус — у него стран­ное кеширо­вание, которое дер­жится сов­сем недол­го, поэто­му для кеширо­вания допол­нитель­но нас­тро­им dnsmasq, ука­зав ему в качес­тве сер­вера cloudflared.

По­это­му надо добавить в /etc/dnsmasq.conf что‑то вро­де таких строк:

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

Ес­ли ответ на зап­рос есть, зна­чит, DNS работа­ет и мож­но запус­тить коман­ду нес­коль­ко раз — при пов­торных зап­росах Query time дол­жен стать 0 мс или око­ло того, если ты зап­рашива­ешь localhost, или будет близ­ко к пин­гу до internal, если дела­ешь это с кли­ента.

Те­перь мож­но добавить в кон­фиги кли­ентов в сек­цию Interface:

DNS = 10.20.30.1, 1.1.1.1

Осо­бые парано­ики могут запус­тить cloudflared на external и скрыть от локаль­ного про­вай­дера даже сам факт исполь­зования DoH. Для это­го в proxy-dns-address в кон­фиге cloudflared и в dnsmasq.conf надо прос­то ука­зать 10.20.30.2 (IP-адрес external).

Кста­ти, в качес­тве аль­тер­нативы мож­но пос­тавить на тот же сер­вер ути­литу Pi-hole, которая дела­ет при­мер­но то же самое, но еще бло­киру­ет рек­ламу и показы­вает кра­сивую ста­тис­тику.

 

Выводы

Па­ры клю­чей в статье — дей­ству­ющие, так что можешь, ничего не исправ­ляя в кон­фигах (толь­ко IP internal-сер­вера), залить их на свои сер­веры и кли­ент и пораз­вле­кать­ся. Но для боево­го при­мене­ния клю­чи надо обя­затель­но переге­нерить.

Бла­го это прос­то, даже если тебе лень раз­бирать­ся с руч­ной генера­цией клю­чей, — я соз­дал неболь­шой bash-скрипт для это­го. Дос­таточ­но сде­лать так на internal-сер­вере:

apt install -y git
git clone https://github.com/vvzvlad/trickster-vpn.git
cd trickster-vpn/config_generator
bash generate_cfgs.sh

Пос­ле чего в пап­ке trickster-vpn/config_generator/configs появят­ся кон­фиги с толь­ко что сге­нери­рован­ными клю­чами, и оста­нет­ся толь­ко ско­пиро­вать wg-internal.conf в /etc/wireguard/wg-internal.conf, wg-external.conf унес­ти на дру­гой сер­вер, а wg-mobile-client.conf исполь­зовать для ноут­бука или телефо­на. Ну и не забыть о проб­росе пор­та, если internal у тебя за NAT.

Ес­ли кто‑то захочет это все кра­сиво обер­нуть в два докера и прик­рутить один из веб‑интерфей­сов для WireGuard (потому что кон­фиги кли­ентов все же удоб­нее соз­давать в нем) — доб­ро пожало­вать на гит­хаб: Trickster VPN.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    9 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии