В этой заметке рассмотрим работу OpenConnect сервера в роли промежуточного сервера для доступа к закрытой инфраструктуре.
Сразу обозначу важный момент: основная цель здесь не в том, чтобы ещё раз поднять обычный ocserv. Этому у меня уже посвящена отдельная инструкция. Здесь интереснее другая часть: middle server, который принимает подключения от пользователей, сам поднимает клиентское OpenConnect-подключение к закрытому контуру и дальше маршрутизирует трафик.
📝 Если вы пока смутно представляете, что такое OpenConnect-сервер, или хотите сначала посмотреть базовую установку ocserv в Docker, рекомендую начать отсюда: Поднимаем OpenConnect SSL VPN сервер (ocserv) в docker для внутренних проектов.
Предисловие
📝 Сперва разберёмся с определением:
📝 Примечание
middle server - это промежуточный сервер между клиентом и основным сервером/сервисом. Его ещё называют proxy, relay, middleware server или jump server - в зависимости от задачи.
В моём случае задача была следующая: есть закрытая инфраструктура, доступная только через защищённую точку входа. При этом мне нужно было выдать доступ пользователям не напрямую в эту точку, а через отдельный промежуточный сервер, которым я могу управлять независимо.
🖐️Эй!
Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐.
Для этого я собрал Docker-образ, в котором рядом с ocserv работает ещё и клиент OpenConnect. Получается такая схема:
- пользователь подключается к middle server;
- middle server держит своё подключение к private server;
- трафик пользователя уходит дальше через клиентский туннель middle server;
- при необходимости через этот туннель уходит не весь трафик, а только выбранные домены и подсети.
☝️ Отдельно подчеркну: в этой статье я называю private server той точкой, куда middle server подключается как клиент. А middle server - это сервер, к которому уже подключаются ваши пользователи.
Чуть подробнее о том, что умеет образ:
- Поднимать обычный OpenConnect-сервер для входящих подключений пользователей;
- Поднимать исходящее OpenConnect-подключение из контейнера к private server;
- Перенаправлять весь клиентский трафик в этот upstream-туннель;
- Перенаправлять только выборочный трафик по списку доменов и подсетей;
- Обновлять списки доменов и маршрутов без перезапуска контейнера;
- Автоматически настраивать nftables, policy routing и dnsmasq;
- Работать с несколькими upstream-профилями для простого failover;
- Управляться через один файл
.env.
📝 Все исходники, в т.ч. Dockerfile для самостоятельной сборки вы найдёте на моём GitHub: https://github.com/r4ven-me/openconnect-middle-server.
Подготовка
Далее предполагается, что у вас есть два Linux-сервера:
- private server - сервер, через который уже доступна закрытая инфраструктура;
- middle server - промежуточный сервер, к которому будут подключаться пользователи.
Также предполагается, что на серверах:
- выполнены базовые настройки Linux;
- у пользователя есть привилегии sudo;
- установлен и запущен Docker Engine.
Я буду использовать реальные домены, чтобы можно было получить валидные TLS-сертификаты от Let’s Encrypt. Для простого теста можно обойтись и внутренними сертификатами: если сертификат не примонтирован в ./data/ssl/live/$OC_SRV_CN/, entrypoint скрипт сгенерирует внутренний CA и серверный сертификат сам.
Вводные данные примера из статьи:
| Сервер | Внешний IP | VPN-подсеть | Роль |
|---|---|---|---|
| private.r4ven.me | 188.227.86.98 | 10.11.11.0/24 | Закрытая точка входа |
| middle.r4ven.me | 188.227.32.92 | 10.10.10.0/24 | Промежуточный сервер для пользователей |
☝️ Подсети OpenConnect на private и middle серверах должны отличаться. Если указать одинаковые подсети, то можно самому себе создать увлекательные приключения в мир маршрутизации 🙃.
Оба сервера работают под управлением Debian GNU/Linux 13-й версии.
Настройка private server
На private server нам нужен обычный ocserv, к которому middle server сможет подключиться как OpenConnect-клиент.
☝️ Если на вашем private server уже работает OpenConnect и вы понимаете, как создать для middle server отдельный .p12, этот раздел можно пролистать. Главное - получить клиентский сертификат, которым middle server будет подключаться к private server.
Подключаемся к private server по защищённому протоколу SSH и работаем от root:
ssh private.r4ven.me
sudo -iСоздаём рабочую директорию:
mkdir -vp /opt/openconnect
cd /opt/openconnectСоздаём файл описания сервисов:
vim compose.yaml---
services:
certbot:
image: certbot/certbot
container_name: certbot
restart: no
stop_grace_period: 5s
cpus: 0.5
mem_limit: 512M
environment:
TZ: ${TZ}
hostname: certbot
volumes:
- ./data/ssl:/etc/letsencrypt
entrypoint: >
sh -c "
certbot certonly --non-interactive --keep-until-expiring --standalone --preferred-challenges http --agree-tos --email ${OC_USER_EMAIL} -d ${OC_SRV_CN}; exit 0;
"
ports:
- "80:80"
openconnect:
depends_on:
certbot:
condition: service_completed_successfully
build:
context: .
dockerfile: Dockerfile
image: r4venme/openconnect:v1.4-client
container_name: openconnect
restart: always
stop_grace_period: 30s
cpus: 1
mem_limit: 512M
cap_add:
- NET_ADMIN
- NET_RAW
hostname: openconnect
env_file:
- .env
volumes:
- ./data/:/etc/ocserv
devices:
- /dev/net/tun:/dev/net/tun
- /dev/vhost-net:/dev/vhost-net
ports:
- "${OC_SRV_PORT}:${OC_SRV_PORT}/tcp"
certbot_renew:
depends_on:
certbot:
condition: service_completed_successfully
image: certbot/certbot
container_name: certbot-renew
restart: unless-stopped
stop_grace_period: 5s
cpus: 0.5
mem_limit: 512M
environment:
TZ: ${TZ}
hostname: certbot-renew
volumes:
- ./data/ssl:/etc/letsencrypt
entrypoint: >
sh -c "
trap exit TERM;
while true; do
certbot renew --non-interactive --keep-until-expiring --standalone --preferred-challenges http --agree-tos;
sleep 24h;
done
"
ports:
- "80:80"📝 Примечание
Как вы могли заметить, помимо сервиса OpenConnect используются еще два: certbot и certbot_renew:
certbot- стартует первым, его завершения ожидают и сервисopenconnectиcertbot_renew. Его задача получить валидные TLS сертификаты от Let’s Encrypt. Для этого ваш сервер должен иметь белый IP адрес и DNS запись типа A, указывающая на этот IP.certbot_renew- запускается в фоновом режиме и работает постоянно. Его задача раз в сутки проверять срок действия текущих сертификатов и при приблежении срока их окончания - обновить их. Таким образом мы не зависим от внешний скриптов/планировщиков, всё делает Docker.
Создаём .env:
vim .envДля private server достаточно серверной части. Параметры клиента и split tunneling оставляем выключенными (хотя при необходимости он также может быть и middle сервером):
# System
TZ="Europe/Moscow"
# Certbot, если используете Let's Encrypt
OC_USER_EMAIL="kar-kar@r4ven.me"
# OpenConnect server
OC_SRV_PORT="443"
OC_SRV_CN="private.r4ven.me"
OC_SRV_CA="R4ven private CA"
OC_IPV4_NET="10.11.11.0"
OC_IPV4_MASK="255.255.255.0"
OC_DNS1="8.8.8.8"
OC_DNS2="8.8.4.4"
OC_CAMOUFLAGE_ENABLE="true"
OC_CAMOUFLAGE_SECRET="secretPrivateServerWord"
OC_CAMOUFLAGE_REALM="Private service"Запускаем:
docker compose up -d
docker compose logs -f
Теперь создадим пользователя, которым middle server будет подключаться к private server:
docker exec -it openconnect ocuser private "Private Client"Готовый файл появится здесь:
ls -l ./data/secrets/private.p12Копируем его во временную директорию, чтобы затем забрать на middle server:
cp -v ./data/secrets/private.p12 /tmp/private.p12
chmod 644 /tmp/private.p12
☝️ Этот .p12 - ключ middle server для подключения к private server. Храните его аккуратно и не используйте один и тот же сертификат для обычных пользователей.
Настройка автозапуска с помощью Systemd
Когда всё проверили, можно настроить автозапуск нашего private server через systemd:
docker compose downcat << EOF > /etc/systemd/system/openconnect.service
[Unit]
Description=OpenConnect middle server
Requires=docker.service
After=docker.service
[Service]
Restart=always
RestartSec=5
WorkingDirectory=/opt/openconnect
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable --now openconnect
journalctl -fu openconnectНастройка middle server
Теперь переходим к основной части. Именно middle server будет принимать пользовательские подключения и решать, куда отправлять их трафик.
Как говорил ранее, есть поддержка двух режимов:
- Full routing - весь трафик пользователя уходит через private server;
- Split routing - через private server уходят только выбранные домены и подсети.
Начнём с общей подготовки.
С помощью scp копируем файл .p12 на middle server, затем подключаемся к нему и также переходим на root:
scp private.r4ven.me:/tmp/private.p12 middle.r4ven.me:/tmp
ssh middle.r4ven.me
sudo -iТеперь создаём рабочую директорию, копируем в неё файл .p12 и создаём файл описания сервисов:
mkdir -vp /opt/openconnect
cd /opt/openconnect
cp /tmp/private.p12 ./
vim compose.yamlНаполняем:
---
services:
certbot:
image: certbot/certbot
container_name: certbot
restart: no
stop_grace_period: 5s
cpus: 0.5
mem_limit: 512M
environment:
TZ: ${TZ}
hostname: certbot
volumes:
- ./data/ssl:/etc/letsencrypt
entrypoint: >
sh -c "
certbot certonly --non-interactive --keep-until-expiring --standalone --preferred-challenges http --agree-tos --email ${OC_USER_EMAIL} -d ${OC_SRV_CN}; exit 0;
"
ports:
- "80:80"
openconnect:
depends_on:
certbot:
condition: service_completed_successfully
build:
context: .
dockerfile: Dockerfile
image: r4venme/openconnect:v1.4-client
container_name: openconnect
restart: always
stop_grace_period: 30s
cpus: 1
mem_limit: 512M
cap_add:
- NET_ADMIN
- NET_RAW
hostname: openconnect
env_file:
- .env
volumes:
- ./data/:/etc/ocserv
- ${OC_CLIENT_0_CERT_FILE}:${OC_CLIENT_0_CERT_FILE}
# - ${OC_CLIENT_1_CERT_FILE}:${OC_CLIENT_1_CERT_FILE}
# - /etc/iproute2/rt_tables.d:/etc/iproute2/rt_tables.d
devices:
- /dev/net/tun:/dev/net/tun
- /dev/vhost-net:/dev/vhost-net
ports:
- "${OC_SRV_PORT}:${OC_SRV_PORT}/tcp"
certbot_renew:
depends_on:
certbot:
condition: service_completed_successfully
image: certbot/certbot
container_name: certbot-renew
restart: unless-stopped
stop_grace_period: 5s
cpus: 0.5
mem_limit: 512M
environment:
TZ: ${TZ}
hostname: certbot-renew
volumes:
- ./data/ssl:/etc/letsencrypt
entrypoint: >
sh -c "
trap exit TERM;
while true; do
certbot renew --non-interactive --keep-until-expiring --standalone --preferred-challenges http --agree-tos;
sleep 24h;
done
"
ports:
- "80:80"Режим Full Routing
Ниже представлена схема сетевого взаимодействия, когда весь клиентский трафик отправляется через upstream-туннель middle server:

Для такого режима включаем OC_CLIENT_ENABLE и добавляем параметры подключения к private серверу:
vim .env# System
TZ="Europe/Moscow"
# Certbot, если используете Let's Encrypt
OC_USER_EMAIL="kar-kar@r4ven.me"
# OpenConnect server
OC_SRV_PORT="443"
OC_SRV_CN="middle.r4ven.me"
OC_SRV_CA="R4ven CA"
OC_IPV4_NET="10.10.10.0"
OC_IPV4_MASK="255.255.255.0"
OC_DNS1="8.8.8.8"
OC_DNS2="8.8.4.4"
OC_CAMOUFLAGE_ENABLE="true"
OC_CAMOUFLAGE_SECRET="secretMiddleServerWord"
OC_CAMOUFLAGE_REALM="Middle service"
# OpenConnect client
# Optional: force physical uplink interface for NAT rules. Empty means auto-detect.
# OC_MAIN_IFACE="eth0"
OC_CLIENT_ENABLE="true"
OC_CLIENT_IFACE="oc-middle"
OC_CLIENT_CHECK_INTERVAL="5"
OC_CLIENT_CHECK_THRESHOLD="3"
# Number of configured OC_CLIENT_<index> profiles.
# Set to "2" when OC_CLIENT_1_* variables are enabled, "3" for OC_CLIENT_2_*, etc.
OC_CLIENT_COUNT="1"
OC_CLIENT_0_SSL_FLAG=true
OC_CLIENT_0_SERVER="private.r4ven.me"
OC_CLIENT_0_SERVER_PORT="443/?secretPrivateServerWord"
OC_CLIENT_0_CERT_FILE="/opt/openconnect/private.p12"
OC_CLIENT_0_CERT_PASS="p12SecretInBase64encode"
OC_CLIENT_0_CHECK_HOST="10.11.11.1"
# OC_CLIENT_1_SSL_FLAG=false
# OC_CLIENT_1_SERVER="private2.example.com"
# OC_CLIENT_1_SERVER_PORT="443/?SecretUrl"
# OC_CLIENT_1_CERT_FILE="/opt/openconnect/private2.p12"
# OC_CLIENT_1_CERT_PASS="p12SecretInBase64encode"
# OC_CLIENT_1_CHECK_HOST="10.12.12.1"Разбор параметров OC_CLIENT
Теперь подробнее разберём параметры, которые отвечают за исходящее OpenConnect-подключение из middle server в private server.
| Параметр | Пример | Описание |
|---|---|---|
OC_CLIENT_ENABLE | "true" | Включает OpenConnect-клиент внутри контейнера. Если значение false, middle server работает как обычный ocserv. |
OC_CLIENT_IFACE | "oc-middle0" | Имя интерфейса, который создаст upstream OpenConnect-клиент. Это имя дальше используется в маршрутах, nftables и split tunneling. |
OC_CLIENT_CHECK_INTERVAL | "5" | Интервал health check в секундах. Скрипт регулярно проверяет доступность OC_CLIENT_<index>_CHECK_HOST через OC_CLIENT_IFACE. |
OC_CLIENT_CHECK_THRESHOLD | "3" | Сколько неудачных проверок подряд нужно получить, прежде чем скрипт начнёт переподключение. |
OC_CLIENT_COUNT | "1" | Количество описанных upstream-профилей. Если используете OC_CLIENT_0_* и OC_CLIENT_1_*, здесь должно быть "2". |
Для каждого upstream-профиля используется индекс: 0, 1, 2 и так далее.
| Параметр | Пример | Описание |
|---|---|---|
OC_CLIENT_0_SSL_FLAG | "true" | Если true, сертификат private server считается доверенным. Если false, клиент дополнительно отвечает yes на подтверждение сертификата. Для боевого использования лучше настроить валидный сертификат и оставить true. |
OC_CLIENT_0_SERVER | "private.r4ven.me" | Адрес private server, к которому будет подключаться OpenConnect-клиент. |
OC_CLIENT_0_SERVER_PORT | "443/?secretPrivateServerWord" | Порт и, при необходимости, camouflage secret. Если camouflage не используется, можно указать просто "443". |
OC_CLIENT_0_CERT_FILE | "/opt/openconnect/private.p12" | Путь к .p12 на хосте и внутри контейнера. |
OC_CLIENT_0_CERT_PASS | "..." | Пароль от .p12, закодированный в base64. |
OC_CLIENT_0_CHECK_HOST | "10.11.11.1" | Адрес, который проверяется через upstream-туннель. Обычно удобно указать адрес самого private server внутри VPN-подсети. |
Логика работы следующая:
occlient.shждёт, пока локальныйocservначнёт слушать порт;- загружает профиль
OC_CLIENT_0_*; - запускает
openconnectс указанным.p12; - создаёт таблицу маршрутизации
430 oc_vpn; - в режиме full routing добавляет для подключившегося пользователя policy rule в таблицу
430; - регулярно проверяет
OC_CLIENT_0_CHECK_HOST; - при проблемах переподключается или переключается на следующий профиль.
☝️Важно
Пароль от .p12 нужно указать в base64. Например:
printf '%s' 'password-for-private-p12' | base64Полученную строку указываем в OC_CLIENT_0_CERT_PASS.
☝️Важно
Файл .p12 должен быть доступен внутри контейнера. Соответственно путь до файла в переменной OC_CLIENT_0_CERT_FILE указывается относительно пути в контейнере.
Проще всего сохранять одинаковый путь на хосте и в контейнере, а в параметрах монтирования в файле compose.yaml указать так:
- ${OC_CLIENT_0_CERT_FILE}:${OC_CLIENT_0_CERT_FILE}Несколько upstream-профилей
Если закрытая инфраструктура доступна через несколько точек входа, можно описать несколько профилей:
OC_CLIENT_COUNT="2"
OC_CLIENT_0_SSL_FLAG="true"
OC_CLIENT_0_SERVER="private.r4ven.me"
OC_CLIENT_0_SERVER_PORT="443/?secretPrivateServerWord"
OC_CLIENT_0_CERT_FILE="/etc/ocserv/secrets/private.p12"
OC_CLIENT_0_CERT_PASS="p12SecretInBase64encode"
OC_CLIENT_0_CHECK_HOST="10.11.11.1"
OC_CLIENT_1_SSL_FLAG="true"
OC_CLIENT_1_SERVER="private2.r4ven.me"
OC_CLIENT_1_SERVER_PORT="443/?secretPrivate2ServerWord"
OC_CLIENT_1_CERT_FILE="/opt/openconnect/private2.p12"
OC_CLIENT_1_CERT_PASS="anotherP12SecretInBase64"
OC_CLIENT_1_CHECK_HOST="10.12.12.1"Если текущий профиль перестаёт проходить health check, скрипт сначала пытается переподключиться к нему же. Если это не помогает, он переключается на следующий профиль. И так по кругу, пока куда-нибудь не подключится и health check будет успешным.
📝 Это не полноценный балансировщик нагрузки. Это простой и понятный failover: одна активная upstream-точка в конкретный момент времени.
☝️ Если определено несколько профилей, то переменная OC_CLIENT_COUNT должна содержать значение равное их количеству: 1, 2, 3 etc.
Запуск middle server
Запускаем middle server:
docker compose up -d
docker compose logs -f
Создаём пользователя уже на middle server:
docker exec -it openconnect ocuser middleuser "Middle User"Копируем клиентский .p12 в /tmp:
cp -v ./data/secrets/middleuser.p12 /tmp/middleuser.p12
chmod 644 /tmp/middleuser.p12Проверка Full Routing
На клиентской машине скачиваем .p12 файл:
scp middle.r4ven.me:/tmp/middleuser.p12 ./Подключаемся к middle server:
sudo openconnect -c ./middleuser.p12 'middle.r4ven.me/?secretMiddleServerWord'Проверяем внешний IP:
curl ifconfig.meВ ответ должен прийти внешний IP private server, потому что весь трафик клиента ушёл через middle server, а затем через upstream-туннель на private server.
Дополнительно можно посмотреть маршрут:
mtr -wb linuxmint.com
Он должен идти через middle server (10.10.10.1) затем через private server (10.11.11.1).
Режим Split Routing
Теперь рассмотрим более интересный режим. В нём пользователь подключается к middle server, но через private server уходит только тот трафик, который мы явно указали.
Например:
ifconfig.meиexample.comотправляем через private server;- подсеть
192.168.25.0/24отправляем через private server; - весь остальной интернет остаётся на стороне клиента и не заворачивается в private server.
Схема получается такая:

Для этого включаем клиентскую часть и добавляем split tunneling:
# System
TZ="Europe/Moscow"
# Certbot, если используете Let's Encrypt
OC_USER_EMAIL="kar-kar@r4ven.me"
# OpenConnect server
OC_SRV_PORT="443"
OC_SRV_CN="middle.r4ven.me"
OC_SRV_CA="R4ven CA"
OC_IPV4_NET="10.10.10.0"
OC_IPV4_MASK="255.255.255.0"
OC_DNS1="8.8.8.8"
OC_DNS2="8.8.4.4"
OC_CAMOUFLAGE_ENABLE="true"
OC_CAMOUFLAGE_SECRET="secretMiddleServerWord"
OC_CAMOUFLAGE_REALM="Middle service"
# OpenConnect client
# Optional: force physical uplink interface for NAT rules. Empty means auto-detect.
# OC_MAIN_IFACE="eth0"
OC_CLIENT_ENABLE="true"
OC_CLIENT_IFACE="oc-middle0"
OC_CLIENT_CHECK_INTERVAL="5"
OC_CLIENT_CHECK_THRESHOLD="3"
# Number of configured OC_CLIENT_<index> profiles.
# Set to "2" when OC_CLIENT_1_* variables are enabled, "3" for OC_CLIENT_2_*, etc.
OC_CLIENT_COUNT="1"
OC_CLIENT_0_SSL_FLAG=true
OC_CLIENT_0_SERVER="private.r4ven.me"
OC_CLIENT_0_SERVER_PORT="443/?secretPrivateServerWord"
OC_CLIENT_0_CERT_FILE="/opt/openconnect/private.p12"
OC_CLIENT_0_CERT_PASS="p12SecretInBase64encode"
OC_CLIENT_0_CHECK_HOST="10.11.11.1"
# Split tunneling
OC_SPLIT_ENABLE="true"
OC_SPLIT_TUNNEL_DNS="true"
OC_SPLIT_ROUTES='
192.168.25.0/24
10.1.0.0/16
1.1.1.1/32
'
OC_SPLIT_DOMAINS='
ifconfig.me
example.com
gnu.org
'Разбор параметров OC_SPLIT
Теперь разберём split tunneling. Он включается только вместе с OC_CLIENT_ENABLE=true, потому что без upstream-туннеля разделять трафик просто некуда.
| Параметр | Пример | Описание |
|---|---|---|
OC_SPLIT_ENABLE | "true" | Включает режим выборочной маршрутизации. Если false, при включённом OC_CLIENT_ENABLE трафик пользователей уходит в upstream-туннель целиком. |
OC_SPLIT_TUNNEL_DNS | "true" | Отправляет DNS-серверы, указанные в OC_DNS1 и OC_DNS2, через upstream-туннель. Полезно, если эти DNS доступны только из private network. |
OC_SPLIT_ROUTES | 192.168.25.0/24 | Начальный список IP-адресов и подсетей, которые нужно отправлять через private server. |
OC_SPLIT_DOMAINS | example.com | Начальный список доменов, IP-адреса которых dnsmasq будет добавлять в nftables set для маршрутизации через private server. |
При включении split tunneling внутри контейнера происходит несколько вещей:
- создаётся таблица маршрутизации
431 oc_split; - добавляется правило
fwmark 0x1 table 431; - для выбранных IP используется маршрут по умолчанию через
OC_CLIENT_IFACE; dnsmasqначинает слушать DNS-адрес middle server внутри VPN-подсети;ocserv.confобновляется так, чтобы VPN-клиенты получили DNS middle server;- домены из списка превращаются в правила
dnsmasqвидаnftset=/domain/4#ip#oc_nat#oc_set; nftablesпомечает IP изoc_set, и только такой трафик уходит в upstream-туннель.
Попробую описать чуть понятнее:
- При запросе к домену из заранее заданного списка
dnsmasqполучает IP-адреса от вышестоящего DNS-сервера, возвращает их клиенту и добавляет в nftset-множество. - Пакеты, направленные к IP-адресам из этого
nftset, маркируются с помощью правил фаервола. - Маркированный трафик перенаправляется в отдельную таблицу маршрутизации, в которой задан маршрут через OpenConnect-туннель ведущий к private server. Остальной трафик идёт через основной интерфейс middle server.
Одним из преимуществ данного способа является формирование nftset по wildcard. Другими словами, вы добавляете домен example.com в список и при обращении к доменам нижнего уровня, например test.example.com, трафик автоматически пойдет через OpenConnect туннель.
Подобную схему я уже ранее реализовывал в статье по подключению роутера на OpenWRT к OpenConnect серверу.
Домены
Домены можно указать сразу в .env:
OC_SPLIT_DOMAINS='
ifconfig.me
example.com
gnu.org
'При первом запуске entrypoint скрипт положит их в файл:
./data/domains.txtПосле запуска удобнее редактировать уже этот файл:
vim ./data/domains.txtФормат простой:
ifconfig.me
example.com
gnu.orgПустые строки и строки с # игнорируются.
Подсети и IP-адреса
Маршруты можно также указывать через .env:
OC_SPLIT_ROUTES='
192.168.25.0/24
10.1.0.0/16
1.1.1.1/32
'Или после запуска редактировать файл:
vim ./data/routes.txtФормат такой же простой:
192.168.25.0/24
10.1.0.0/16
1.1.1.1/32Изменения в domains.txt и routes.txt подхватываются автоматически. Контейнер перезапускать не нужно. В случае обновления переменных в .env придётся перезапускать контейнер.
Запуск middle server
docker compose down
docker compose up -d
docker compose logs -f
Проверка Split Routing
Подключаемся к middle server на клиенте:
sudo openconnect -c ./middleuser.p12 'middle.r4ven.me/?secretMiddleServerWord'Проверяем домены из списка:
curl ifconfig.me
curl eth0.meЕсли ifconfig.me добавлен в OC_SPLIT_DOMAINS, он должен открываться через private сервера. Если eth0.me в список не добавляли, он должен показать внешний IP middle сервера.

Проверяем маршруты:
mtr -wb example.com
mtr -wb linuxmint.com
mtr -wb 1.1.1.1Обратите внимание на второй hop: для адресов из списка трафик должен уходить в сторону middle/private цепочки, а для остальных адресов - идти обычным путём.

Пример обновления списков доменов и подсетей на лету
Например, добавим домен и отдельный IP:
echo 'eth0.me' >> ./data/domains.txt
echo '9.9.9.9/32' >> ./data/routes.txtЧерез некоторое время на клиенте можно сбросить DNS-кеш и проверить:
resolvectl flush-caches
curl eth0.me
mtr -wb 9.9.9.9
📝 Для доменов важно, чтобы клиент реально использовал DNS, который выдал ocserv. Если на клиенте включён DoH/DoT или жёстко прописан внешний DNS, доменный split tunneling может работать непредсказуемо или не работать вовсе.
Запуск в режиме network_mode: host
Хоть и работа сервисов в изолированной Docker среде - это преимущество, оно же имеет и недостатки. В случае работы клиентского подключения к private server внутри контейнера, скорость клиентов, подключённых к middle server может быть, в среднем, в 3 раза ниже.
Если вам критически важна скорость, то можно запустить middle server в сетевом режиме хоста. Для этого просто замените директиву ports на network_mode: host.
Пример:
---
services:
openconnect:
depends_on:
certbot:
condition: service_completed_successfully
build:
context: .
dockerfile: Dockerfile
image: r4venme/openconnect:v1.4-client
container_name: openconnect
restart: always
stop_grace_period: 30s
cpus: 1
mem_limit: 512M
cap_add:
- NET_ADMIN
- NET_RAW
hostname: openconnect
env_file:
- .env
volumes:
- ./data/:/etc/ocserv
devices:
- /dev/net/tun:/dev/net/tun
network_mode: hostЯ постарался по максимуму адаптировать логику работы скриптов внутри контейнера так, чтобы они идентичным образом работали и в сетевом режиме хоста.
Однако!
Будьте осторожны, т.к. при работе в таком режиме скрипты будут вносить изменения в сетевые параметры самого хоста, а конкретнее: управлять маршуртами и правилами маршрутизации, а также настройками фаервола с помощью nftbales.
Настройка автозапуска с помощью systemd
Когда всё проверили, можно настроить автозапуск нашего middle server через systemd:
docker compose downcat << EOF > /etc/systemd/system/openconnect.service
[Unit]
Description=OpenConnect **middle server**
Requires=docker.service
After=docker.service
[Service]
Restart=always
RestartSec=5
WorkingDirectory=/opt/openconnect
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable --now openconnect
journalctl -fu openconnectПолезные команды для диагностики
Посмотреть логи контейнера:
docker compose logs -f openconnectПроверить, поднялся ли upstream-интерфейс:
docker exec -it openconnect ip addr show oc-middle0Посмотреть policy routing:
docker exec -it openconnect ip rule show
docker exec -it openconnect ip route show table 430
docker exec -it openconnect ip route show table 431Посмотреть nftables:
docker exec -it openconnect nft list table ip oc_nat
docker exec -it openconnect nft list chain DOCKER-USERПосмотреть текущие домены и маршруты split tunneling:
cat ./data/domains.txt
cat ./data/routes.txtПри необходимости можно вручную очистить nftables set с доменными IP:
docker exec -it openconnect nft flush set ip oc_nat oc_setПослесловие
Во время разработки образа было выпито не мало кружек чая и потрачено сильно больше нервов, чем планировалось. Но результат того стоил, я считаю.
В итоге получился очень удобный и гибкий инструмент для управления маршрутизацией внутри приватных сетей.
Пользователи подключаются к middle server, а дальше вы сами выбираете логику: отправлять весь их трафик через private server или пропускать туда только нужные домены и подсети.
Мне такой подход нравится тем, что он не требует раздавать пользователям прямой доступ к private server. Private-контур остаётся закрытым, а middle server можно отдельно обновлять, переносить, ограничивать и использовать как управляемую точку входа.
Уверен у многих пользователей могут возникнут различные вопросы. Смело задавайте их в Вороньем чате, в issue на GitHub или пишите мне на почту: kar-kar@r4ven.me.
Спасибо, что читаете. Всех благ!


