На Хабре нередко появляются статьи типа "я хотел настроить <черный ящик>, не углубляясь в доки подергал за рычаги и у меня всё получилось, сейчас я научу вас как надо". Не смотря на неэкспертный технический уровень они нередко вызывают интерес и бурные дискуссии. Поэтому я решил вам рассказать про сисадмина Васю, который решил настроить себе двухзвенный VPN из подручных средств не выходя далеко за пределы Хабра.
Вася любил читать хабр, и прочитав про ocserv немедленно скачал, поставил и настроил. И работал он у него на всех доступных устройствах и блокировки всегда обходили его стороной. Но когда в новостях написли что вот-вот заблокируют вообще всё, Вася задумался о двухзвенной схеме, которую опытные эксперты на хабре называли перспективной. При этом Вася не хотел переходить на новое ПО, потому, что везде где надо всё уже привычно стояло и работало, и хотелось, в случае необходимости, по-быстренькому поменять адреса и продолжать работать, а не искать, ставить, настраивать и привыкать заново. Кроме того, все известные Васе модные vpn протоколы периодически блокировали, дорабатывали, снова блоркировали и снова дорабатывали, и так по кругу, а ocserv работал себе потихоньку и не подводил ни разу.
В голове сразу возник план будущего проекта:

и вот, сидя на выходных дома с простудой, Вася приступил к работе. Скачал свежие исходники ocserv с gitlab установил в /opt/ocserv, получил ssl сертификат от Let's Encrypt, и нарисовал базовый конфиг:
ocserv.cfg
auth = "plain[passwd=/opt/ocserv/etc/ocserv.passwd]" listen-host = <адрес входного сервера> udp-listen-host = 127.0.0.1 # отключаем DTLS чтобы меньше быть похожими на vpn ipv4-network = 172.16.20.0/24 # диапазон клиентских адресов route = default # нужно для клиентов, не нужно для выходного сервера tcp-port = 443 udp-port = 443 server-cert = /etc/letsencrypt/live/<DNS имя входного сервера>/fullchain.pem server-key = /etc/letsencrypt/live/<DNS имя входного сервера>/privkey.pem dh-params = /etc/cron.d/dh.pem use-occtl = true occtl-socket-file = /run/occtl.socket socket-file = /run/ocserv-socket device = biteme predictable-ips = true compression = false tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE:%COMPAT:-VERS-ALL:+VERS-TLS1.2:-ARCFOUR-128" max-ban-score = 50 ban-reset-time = 300 ban-points-wrong-password = 10 ban-points-connection = 1 camouflage = true camouflage_secret = "<секретный код>" camouflage_realm = "Please Login."
Подключившись к входному серверу с выходного сервера и с домашнего компьютера получил два новых интерфейса:
Фрагмент вывода ip a
3: biteme0: <POINTOPOINT,UP,LOWER_UP> mtu 1472 qdisc fq_codel state UNKNOWN group default qlen 500 link/none inet 172.16.20.1 peer 172.16.20.2/32 scope global biteme0 valid_lft forever preferred_lft forever 4: biteme1: <POINTOPOINT,UP,LOWER_UP> mtu 1472 qdisc fq_codel state UNKNOWN group default qlen 500 link/none inet 172.16.20.1 peer 172.16.20.3/32 scope global biteme1 valid_lft forever preferred_lft forever
Дело за малым, осталось зарутить всё с biteme1 на biteme0 и всех делов. Но, повозившись, Вася обнаружил, что номера, имена и адреса интерфейсов зависят от порядка подключения, и, прежде чем маршрутизировать, нужно понять куда и откуда.
Чтобы получить фиксированный адрес клиента пришлось выйти за пределы хабра, но ненадолго, и в итоге Вася добился фиксированного адреса для клиента с выходного сервера:
ocserv.conf
... ipv4-network = 172.16.20.128/25 route = 172.16.20.0/24 config-per-user = /opt/ocserv/etc/config-per-user/ ...
/opt/ocserv/etc/config-per-user/exitserver
ipv4-network = 172.16.20.0/30 route = 172.16.20.0/24
Ocserv не позволяет задавать фиксированные адреса клиентам, но задав маску посети /30 можно оставить два бита для адреса, тогда адрес 1 (01) будет использован для серверного интефейса, адрес 2 (10) - для клиентского. После этих дополнений выходной сервер стал всегда получать адрес 172.16.20.2, а клиентские устройства - адреса в диапазоне 172.16.20.130..255.
С маршрутизацией всё оказалось сложнее. Пушистые пакетики с домашнего кома бодро рутились на входной сервер, но там, потерявших берега и ориентиры, их уносило в дикий интернет через default route. Не долго думая Вася сделал:
ip route add <адрес входного сервера> via <default route входного сервера> ip route add <адрес выходного сервера> via <default toute выходного сервера> ip route replace default via 172.16.20.2 #vpn адрес на выходном сервере
И всё сразу заработало (потому, что маскарадинг на выходном сервере у Васи уже был настроен). Но возникла очевидная проблема: теперь к входному серверу можно подключиться только с адресов для которых прописаны статические маршруты. Одевшись, Вася пошел прогуляться (не за пивом, а за ингалиптом и нозанексом), думая по дороге, что можно бы завернуть ocserv в docker, но это уже несколько черезчур, и, внезапно вспомнил, что когда-то читал на хабре статью про network namespaces. Немедленно возник план: создаем network namespace, в нем стартуем ocserv, в нем-же прописываем default gateway и таким образом настоящий default gateway не пострадает. А как же попадать в network namespace снаружи? Конечно через nginx reverse proxy. Красиво? Нет. Криво? Да. Но бросать начатое, и почти готовое, на полпути не хотелось, поэтому, прежде чем вздохнуть и сделать по уму, Вася решил довести дело до конца. Ну, или тупика.
Конфиг nginx получился лаконичным:
nginx.conf
include /etc/nginx/modules-enabled/*.conf; events { worker_connections 30; } stream { upstream ocserv { server 192.168.0.2:443; # адрес в network namespace } server { listen <адрес входного сервера>:443; # смотрит в дикий интернет proxy_pass ocserv; proxy_protocol on; } }
Адрес 192.168.0.2 - это адрес сетевого интерфейса в network namespace в котором будет запущен ocserv. Сам ocserv запускается юнитом:
ocserv.service
[Unit] Description=OpenConnect SSL VPN server Documentation=man:ocserv(8) After=network-online.target [Service] PrivateTmp=true PIDFile=/run/ocserv.pid Type=simple ExecStartPre=/opt/ocserv/netnsctl start ExecStart=ip netns exec ocservns /opt/ocserv/sbin/ocserv --foreground --pid-file /run/ocserv.pid --config /opt/ocserv/etc/ocserv.conf ExecStopPost=/opt/ocserv/netnsctl stop ExecReload=/opt/ocserv/bin/occtl reload [Install] WantedBy=multi-user.target
Скрипт для создания/удаления network namespace Вася скопипастил из упомянутой статьи, подправил и получилось следующее:
netnsctl
#!/bin/bash set -e export nsname=ocservns netns_use() { echo Use: $0 '<create|delete>' exit } netns_create () { ip netns add ${nsname} ip netns exec ${nsname} ip link set dev lo up ip link add ${nsname}p type veth peer name ${nsname}c ip link set ${nsname}c netns ${nsname} ip addr add 192.168.0.1/24 dev ${nsname}p ip link set dev ${nsname}p up ip netns exec ${nsname} ip addr add 192.168.0.2/24 dev ${nsname}c ip netns exec ${nsname} ip link set dev ${nsname}c up ip netns exec ${nsname} ip route add default via 192.168.0.1 } netns_delete () { ip netns del $i } case "$1" in create) netns_create ;; delete) netns_delete ;; *) netns_use ;; esac
Чтобы всё заработало осталось включить proxy protocol в ocserv и добавить default route через 172.16.20.2. Поскольку ocserv создает сетевые интерфейсы только при подключении клиентов, пришлось добавить скрипт, запускаемый при подключении клиента с выходного сервера:
ocserv-connect
#!/bin/bash #Следующая строка не нужна, но будет работать правильно, потому что используется proxy protocol #echo "$(date) [info] User ${USERNAME} Connected - Server: ${IP_REAL_LOCAL} VPN IP: ${IP_REMOTE} Remote IP: ${IP_REAL} Device:${DEVICE}" >> /var/log/ocserv.log if [ ${IP_REMOTE} == "172.16.20.2" ] then /usr/sbin/ip netns exec ocservns ip route add default via 172.16.20.2 fi
Чувствуя себя художником, Вася внес последние штрихи:
ocserv.conf
auth = "plain[passwd=/opt/ocserv/etc/ocserv.passwd]" listen-host = 192.168.0.2 #адрес в network namespace udp-listen-host = 127.0.0.1 ipv4-network = 172.16.20.128/25 route = 172.16.20.0/24 route = default listen-proxy-proto = true # для работы через proxy и верного отображения ip клиентов config-per-user = /opt/ocserv/etc/config-per-user/ # конфигурация юзера выходного сервера connect-script = /opt/ocserv/bin/ocserv-connect # для установки default route #disconnect-script = /opt/ocserv/scripts/ocserv-disconnect listen-proxy-proto = true tcp-port = 443 udp-port = 443 server-cert = /etc/letsencrypt/live/<DNS имя входного сервера>/fullchain.pem server-key = /etc/letsencrypt/live/<DNS имя входного сервера>/privkey.pem dh-params = /etc/cron.d/dh.pem use-occtl = true occtl-socket-file = /run/occtl.socket socket-file = /run/ocserv-socket device = biteme predictable-ips = true compression = false tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE:%COMPAT:-VERS-ALL:+VERS-TLS1.2:-ARCFOUR-128" max-ban-score = 50 ban-reset-time = 300 ban-points-wrong-password = 10 ban-points-connection = 1 camouflage = true camouflage_secret = "<секретный код>" camouflage_realm = "Please Login."
После этого осталось запустить сервер и клиентов и посмотреть как это работает. Поскольку входной серер васполагался примерно по пути от домашнего компа до выходного сервера, ping roundtrip time вырос менее чем на <=10% по сравнению с vpn соединеним через openconnect и zerotier. А при скачивании freebsd.iso с mirror.yandex.ru скорость упиралась в 100Мбит - скорость интерфейса на выходном сервере, поэтому при всех недостатках сетап оказался вполне рабочим, Вася почувствовал себя лучше и простуда с насморком сразу отступили.
