
Привет! Я Ваня, системный администратор в Selectel. Представим ситуацию: вам нужно организовать доступ к удаленному офису через Cisco AnyConnect, который по какой-либо причине должен работать на стандартном 443 порте. При этом на том же порте развернут веб-сервер, а позже понадобится там же развернуть FTPS, SSTP и еще 100 500 сервисов, которые используют TLS.
Используйте навигацию, если не хотите читать текст целиком:
Материал подготовлен исключительно в образовательных целях. Описанные способы настройки VPN и маршрутизации трафика не предназначены для обхода блокировок или иных ограничений, установленных законодательством РФ.
Идея решения
На первый взгляд может показаться, что разместить несколько сервисов с TLS на одном порте невозможно. Но достаточно вспомнить об обратных прокси (реверс-прокси), которые могут слушать один порт, а затем проксировать входящие соединения к нужным сервисам на других портах.
Очевидным решением кажется осуществлять TLS-терминацию на стороне реверс-прокси, а расшифрованный трафик отправлять в нужный сервис (предварительно разобрав, для кого трафик предназначен). Но в случае с AnyConnect этот подход не работает: TLS — неотъемлемая часть AnyConnect, а выполнить терминацию со стороны реверс-прокси нельзя.
Важно отметить, что теоретически возможны варианты без TLS, но на момент написания статьи таких реализаций AnyConnect не существует.
Далее придется работать с TLS, не расшифровывая его. Но чтобы все же определить, к какому сервису относится запрос, нужно использовать информацию, которая передается до установки шифрования. Для этого разберем, как устроен TLS.
Как устроен TLS
На этапе установки соединения TLS выполняет несколько шагов, известных как «рукопожатие» (handshake). Важный момент: шифрование начинается не сразу, а лишь на втором этапе — ClientKeyExchange.

До обмена ключами клиент и сервер передают часть данных в открытом виде, если не используется TLS 1.3 с ECH. Клиент начинает установку соединения, отправляя сообщение следующего вида (ClientHello):
struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
CipherSuite cipher_suites;
CompressionMethod compression_methods;
Extension extensions;
} ClientHello;
Внимательные и просвещенные сразу могли заметить, что получив подобный запрос от клиента сервер не знает, какой сертификат отправлять клиенту в ответ, так что отправит тот, что указан в default_server. Но ведь тысячи сайтов с разными сертификатами каким-то образом уживаются на одном веб сервере? Ищем расширения для TLS в RFC 6066 и вскоре находим SNI — Server Name Indication. Суть расширения SNI проста — разберем ее на пальцах.
Клиент указывает в ClientHello имя сервера, к которому хочет подключиться.
Сервер, увидев это имя, передает клиенту соответствующий сертификат.
Клиент и сервер устанавливают защищенное соединение, а мы теряем возможность разобрать со стороны, что за трафик ходит между ними.
Однако информации из SNI в этом случае уже достаточно, чтобы определить, к какому сервису нужно направить подключение. Каждому TLS-сервису назначаем уникальное имя, а далее — подсматриваем за процессом установки TLS-соединения. В зависимости от значения SNI направляем входящий трафик в сервис, для которого он предназначен.

30+ бесплатных курсов на IT-темы в Академии Selectel
Для начинающих и опытных специалистов.
Реализация
Переходим к практике. Исторически во всех своих проектах на протяжении восьми лет я использовал Nginx, поэтому первым делом я проверил, можно ли реализовать подобную схему на нем. И ответ оказался положительным.
Помимо того, что Nginx — это веб-сервер, он также способен работать как обратный прокси. При этом не только для HTTP(S), но и для TCP-соединений, а при необходимости — для SMTP, POP3 и IMAP. Однако для обратного проксирования TCP и «подглядывания» в SNI нам понадобятся модули, которые могут отсутствовать в стандартной сборке Nginx. Проверьте их наличие командой nginx -V
. В выводе должны присутствовать флаги --with-stream
и --with-stream_ssl_preread_module
.
Далее — все просто: для экономии ресурсов удаляем все остальные (ненужные) модули, файлы конфигурации. Оставляем только nginx.conf
со следующим содержимым:
# Запуск от имени непривилегированного пользователя
user nginx;
# Автоматический выбор количество worker-процессов по количеству ядер процессора
worker_processes auto;
# пути до лога ошибок и PID-файла
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
# максимальное количество соединений — это количество worker-процессов: число соединений для одного worker. Устанавливайте в зависимости от количества worker и нагрузки на ресурсы за Nginx.
events {
worker_connections 4096;
}
# TCP-проксирование с роутингом по SNI:
stream {
map $ssl_preread_server_name $name {
anyconnect.example.com anyconnect;
default web;
}
upstream web {
server 127.0.0.1:8443;
}
upstream anyconnect {
server 127.0.0.1:8444;
}
server {
listen 443;
proxy_pass $name;
ssl_preread on;
}
# формат лога доступа
log_format stream_log '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$ssl_preread_server_name" '
'-> $upstream_addr';
# путь к файлу лога доступа
access_log /var/log/nginx/stream_access.log stream_log;
}
Большая часть директив здесь вполне очевидна, но разберем несколько ключевых моментов:
map
— связывает значение$ssl_preread_server_name
(имя сервера из SNI) с переменной$name
.upstream
— описывает реальные адреса и порты сервисов, на которые нужно направлять трафик.ssl_preread on
— включает «предпросмотр» TLS-рукопожатия, чтобы Nginx мог прочитать SNI до установления зашифрованного соединения.
Таким образом, один порт (443) используется сразу для нескольких сервисов, а маршрутизация выполняется на основании имени сервера, запрошенного клиентом. При необходимости вы можете расширить конфигурацию до любого количества сервисов.
На этом можно было бы закончить, однако в случае, если таким образом вы пытались подружить VPN с web, то наверняка захотите обойтись одним лишь роутером. Если доступ к офису или homelab изве будет зависеть от работоспособности еще одного виртуального/железного сервера, то это создаст лишнюю точку отказа: при поломке VPN-сервера придется физически ехать «на адрес». Сломался сервер с Nginx — все равно ехать.
С OpenWRT, pfSense и OPNSense проблем нет, Nginx там можно установить пакетным менеджером, однако Mikrotik все немного сложнее. В них нет ни привычного шелла, ни пакетного менеджера. Но RouterOS 7.5 сильно расширила возможности «микротов», добавив поддержку контейнеров, совместимых с Docker. Теперь можно без проблем накатить на Mikrotik Nginx и «подложить» ему приведенный выше конфигурационный файл.
Дружим Nginx и Mikrotik
Перед тем как начать, убедитесь, что:
роутер построен на x86 или ARMv8,
есть хотя бы 192 МБ свободной оперативной памяти,
установлен container-пакет с официального сайта Mikrotik,
включена возможность использования контейнеров.
Выполнить последний пункт можно с помощью команды:
/system/device-mode/update container=yes
1. Создаем интерфейс для Nginx-контейнера и bridge для контейнеров:
/interface/veth/add name=nginx address=172.17.0.2/24 gateway=172.17.0.1
/interface/bridge/add name=docker
/ip/address/add address=172.17.0.1/24 interface=docker
/interface/bridge/port add bridge=docker interface=nginx
2. Настраиваем NAT, чтобы процессы из контейнеров могли выходить в другие сети:
/ip/firewall/nat/add chain=srcnat action=masquerade src-address=172.17.0.0/24
3. Выносим конфиг Nginx и его логи за пределы контейнера:
/container mounts
add dst=/etc/nginx name=nginx-etc src=/usb1/nginx/etc
add dst=/var/log name=nginx-logs src=/usb1/nginx/logs
src
измените в зависимости от выбранного пути хранения.
4. Настраиваем источник контейнеров (Registry), откуда Mikrotik будет подтягивать образы. И здесь есть важный нюанс: все инструкции по контейнерам для «микротов», с которыми я ознакомился, использовали что-то подобное:
/container/config/set registry-url=https://registry-1.docker.io tmpdir=/pull
Но у меня это не заработало, Registry докер хаба отдавал 401. Зато отлично работал quay.io — публичный реестр образов Red Hat. Если из каких-либо соображений не хотите его использовать или предпочитаете Docker Hub, образов в котором попросту больше, то можно воспользоваться обходным решением.
1. Не задаем registry-url
.
2. При копировании образа указываем полный путь к Docker Hub:
/container/add remote-image=registry-1.docker.io/library/nginx:1.29.1-alpine interface=nginx root-dir=/docker/nginx mounts=nginx-etc,nginx-logs.
Финальные штрихи и заключение
Теперь ваш Mikrotik готов к работе с Nginx:
поместите подготовленный конфигурационный файл в смонтированный каталог (
/usb1/nginx/etc
);проверьте правила firewall;
создайте dst-nat правило, чтобы трафик на порт 443 направлялся в контейнер.
Поздравляем, ваш Mikrotik превратился в полноценный веб-сервер. ? Теперь один порт (443) может обслуживать несколько сервисов — например, веб-сервер, VPN для удаленной работы и FTPS — без конфликтов и лишних виртуальных машин. Такой подход сэкономит вам ресурсы, повысит надежность (меньше точек отказа), а также сохранит гибкость: вы смело можете добавлять новые TLS-сервисы без изменения схемы.
init0
Учитывая то, что
и у вас вряд ли один процессор - я бы рекомендовал добавить
reuseport
вlisten
стрим сервера