Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Два TLS-сервиса на одном 443-м порту: разбор по SNI

Дата публикации: 15-09-2026 11:02:32

Один публичный IP, один 443-й порт, на нём уже работает сервис с живыми пользователями — и туда же нужно посадить второй. Менять настройки на устройствах пользователей нельзя.Разбираю, как разделить порт между двумя TLS-сервисами, ничего не расшифровывая: SNI уходит открытым текстом в ClientHello, и nginx умеет читать его директивой ssl_preread, оставаясь для TLS прозрачным. Отдельно — почему всё держится на ветке default, чем за прозрачность платят (бэкенд перестаёт видеть реальные адреса клиентов) и когда у этого решения закончится срок годности. Читать далее

Основное содержимое страницы с новостью.

Уровень сложностиСредний

Время на прочтение5 мин

Охват и читатели9.5K

Кейс

Задача звучит просто: есть один публичный IP и один 443-й порт, на нём уже работает сервис с живыми пользователями, и туда же нужно посадить второй. Менять что-либо на устройствах пользователей нельзя.

Один белый адрес, за ним и веб-сервис, и VPN-концентратор. Перенести второй на нестандартный порт не выходит: либо его режут по дороге корпоративные файрволы и гостевые сети, либо у клиентов физически нет возможности переключиться: конфиги розданы, устройства в поле, до половины из них вы не дотянетесь.

Разбирать буду на своём стенде: VPS, на котором стоит SoftEther (SSTP на 443, L2TP/IPsec и OpenVPN на своих портах), и рядом понадобилось поднять обычный HTTPS-сайт. Ограничения такие:

  • 443 занят VPN-сервисом, у которого уже есть подключённые клиенты;

  • настройки на устройствах пользователей трогать нельзя;

  • машина одноядерная, 973 МБ памяти — «поднять ещё одну такую же» некуда.

Почему очевидные варианты не подходят

Перенести один сервис на другой порт. Ломает существующих клиентов, а для VPN ещё и снижает проходимость: 443 — единственный порт, который открыт практически везде.

Взять второй IP-адрес. Работает, но это деньги, а у многих провайдеров дополнительный адрес ещё и не выдаётся моментально. Плюс не решает задачу, если клиенты уже прибиты к конкретному адресу.

Разложить по путям на уровне HTTP. Не годится: чтобы увидеть URL, надо терминировать TLS. А во второй ветке у нас не HTTP вообще, там свой протокол поверх TLS.

Остаётся четвёртый уровень: научиться понимать, кому адресовано соединение, до того как оно расшифровано.

SNI виден до расшифровки

Когда клиент начинает TLS-рукопожатие, он отправляет ClientHello. В нём среди прочего лежит расширение SNI (Server Name Indication) — имя хоста, к которому клиент идёт. Расширение придумали ровно для того, чтобы за одним IP можно было держать несколько виртуальных хостов с разными сертификатами: сервер должен узнать имя раньше, чем выберет, какой сертификат предъявить.

SNI передаётся открытым текстом. Он уходит в самом первом пакете, до того как согласован общий ключ. Прочитать его можно, ничего не расшифровывая и не имея приватного ключа.

На этом и строится маршрутизация. Модуль stream в nginx умеет подглядывать в ClientHello директивой ssl_preread, класть имя в переменную $ssl_preread_server_name и дальше проксировать соединение байт в байт — для TLS он остаётся полностью прозрачным, рукопожатие происходит между клиентом и настоящим бэкендом.

stream {
    map $ssl_preread_server_name $upstream_443 {
        www.example.com   127.0.0.1:8444;   # HTTPS-сайт
        default           127.0.0.1:4443;   # SoftEther
    }

    server {
        listen 443 reuseport;
        listen [::]:443 reuseport;

        ssl_preread   on;
        proxy_pass    $upstream_443;

        proxy_timeout         300s;
        proxy_connect_timeout 5s;
    }
}

Модуль ngx_stream_ssl_preread_module собирается в nginx по умолчанию начиная с 1.11.5, отдельно ставить обычно ничего не нужно — но проверить стоит:

nginx -V 2>&1 | tr ' ' '\n' | grep preread

Сам VPN-сервис при этом переезжает с 0.0.0.0:443 на 127.0.0.1:4443. Снаружи он больше не виден, весь вход идёт через nginx.

Ветка default — то, на чём всё держится

Самая важная строчка в этой конфигурации — default.

Старые клиенты подключаются по IP-адресу, а значит SNI не отправляют вообще. Расширение существует, чтобы различать хосты по имени; при обращении к голому IP отправлять в нём нечего, и клиент его просто не добавляет. Так же ведут себя многие VPN-клиенты, даже когда в настройках у них указано доменное имя.

Пустой SNI не совпадёт ни с одним именем в карте и уйдёт в ветку по умолчанию. Именно поэтому у пользователей ничего не меняется: они как ходили на IP:443, так и ходят, а разбор происходит до того, как их трафик кого-либо касается.

Проверяется это одной командой, с именем и без:

openssl s_client -connect 203.0.113.10:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject
openssl s_client -connect 203.0.113.10:443 </dev/null 2>/dev/null | openssl x509 -noout -subject

Первая должна вернуть сертификат сайта, вторая — сертификат VPN-сервиса. Если обе возвращают одно и то же, ssl_preread не включился или карта не подхватилась.

Чем за это платим

Плата ровно одна, но о ней надо знать заранее: бэкенд перестаёт видеть реальные адреса клиентов. Для него все соединения приходят с 127.0.0.1.

Штатное лекарство — протокол PROXY: nginx добавляет перед потоком служебную строку с исходным адресом, бэкенд её разбирает. Проблема в том, что понимать её должен именно бэкенд, а SoftEther этого не умеет. Вторая деталь:

в блоке stream директива proxy_protocol задаётся на уровне server, а не upstream.

То есть включить её для одной ветки и не включить для другой в рамках одного server не получится — либо для обеих, либо ни для одной. Раз одна из сторон протокол не понимает, остаётся «ни для одной».

Обойти можно через iptables TPROXY с прозрачным проксированием, но это заметно сложнее в эксплуатации. Если по адресам клиентов вы никого не считаете и ограничений по IP не строите, проще принять как есть, а видимость вернуть логом самого роутера:

log_format sni_route '$remote_addr [$time_local] '
                     'sni="$ssl_preread_server_name" -> $upstream_addr '
                     'status=$status bytes=$bytes_sent/$bytes_received '
                     'dur=$session_time';

Он пишет настоящий адрес источника и то, в какую ветку ушло соединение. Для эксплуатации этого достаточно, а заодно сразу видно распределение трафика между сервисами. Логирование в stream доступно начиная с nginx 1.11.4.

Ёмкость: считайте соединения дважды

При переезде на L4-прокси каждая сессия занимает два слота соединений — со стороны клиента и со стороны бэкенда. Раньше сервис слушал порт сам и в бюджет nginx не входил вовсе.

В Debian значение по умолчанию — worker_connections 768. При одном рабочем процессе это потолок примерно в 384 одновременные сессии через роутер, и упереться в него можно неожиданно быстро — особенно если один из сервисов за этим портом живёт на коротких соединениях. Симптом в error.log характерный:

[alert] 768 worker_connections are not enough while connecting to upstream

Разумная база:

worker_processes      auto;
worker_rlimit_nofile  65535;

events {
    worker_connections 16384;
    multi_accept       on;
}

worker_rlimit_nofile поднимать обязательно вместе с worker_connections — иначе упрётесь в лимит файловых дескрипторов раньше, чем в лимит соединений, и получите совсем другую ошибку, про Too many open files.

Срок годности решения

Разрабатывается ECH (Encrypted Client Hello) — механизм, который шифрует внутренний ClientHello целиком, включая SNI. Наружу при этом уходит «внешнее» имя, общее для всех сайтов за одним фронтендом. По мере того как ECH будет распространяться, маршрутизация по $ssl_preread_server_name начнёт видеть именно это внешнее имя, а не то, куда клиент на самом деле идёт.

Для схемы вроде описанной это не мгновенная смерть: ECH включается согласованно обеими сторонами, и ваш собственный бэкенд его просто не анонсирует. Но закладываться на SNI как на постоянную опору не стоит.

Альтернативы

nginx здесь не уникален:

  • HAProxy делает то же самое в режиме tcp через req.ssl_sni, и правила у него гибче — можно матчить по суффиксу и строить ACL посложнее;

  • sslh мультиплексирует по типу протокола, а не по имени: различает SSH, TLS, OpenVPN и другие на одном порту. Если задача не «два TLS», а «TLS и SSH» — брать надо его.

nginx удобен, если он у вас на машине уже есть: тогда это двадцать строк в конфиг, без нового компонента в эксплуатации.

Что забрать из этого текста
  • SNI читается до расшифровки, маршрутизация строится без терминирования TLS.

  • Клиенты по IP не отправляют SNI и всегда попадают в default — на этом держится бесшовность миграции.

  • Бэкенд перестаёт видеть адреса клиентов, и это не лечится точечно: proxy_protocol в stream включается на весь server.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Мой бесплатный сервис чуть не стал сканером портов для всего интернета07.0328-09-2026
2Nginx Proxy Manager на облачных серверах — установка, настройка, порядок в URL’ах010.407-10-2026
3Где HTTP/2 и HTTP/3 реально быстрее, а где не дают ничего015.1514-09-2026
4TLS‑сертификатам урезали жизнь до 47 дней. Готовьте автоматизацию, время пошло011.3522-09-2026
5Мониторинг SSL: как не пропустить истекающий сертификат013.8508-10-2026
6Мониторинг SSL сертификатов в Zabbix07.3928-09-2026
7Джуну дали ИИ, а работы прибавилось у сеньора09.7728-09-2026
8У вас 64 ядра и 256 ГБ памяти, но Nginx ...014.4229-09-2026
9Address already in use: как найти, кто занял порт, и почему он не всегда освобождается сразу-15.3602-10-2026
10Консоль починки сети по аудиокабелю: когда SSH уже бесполезен08.2601-10-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 10.1. Источник: habr.com.