Stream (SNI-роутинг) в SWAG
📘 Гайд: Stream (SNI-роутинг) в SWAG — полная документация
Актуально для: SWAG (linuxserver/docker-swag) + nginx 1.25+
Последнее обновление: 2026-05-27
📑 Содержание
- Что такое stream и зачем он нужен
- Как это работает (схема)
- Какие проблемы решает
- Файловая структура — что куда класть
- Шаг 1: nginx.conf — добавить блок stream
- Шаг 2: stream.conf — SNI-роутинг
- Шаг 3: default.conf — перевести http на порт 4443
- Шаг 4: site-confs — все server слушают 4443
- Боевой пример конфигурации
- Проверка и отладка
- Типичные ошибки и решения
- Docker Compose — порты
1. Что такое stream и зачем он нужен
Stream — это модуль nginx для работы на L4 (TCP/UDP) уровне, в отличие от стандартного http блока, который работает на L7 (HTTP) уровне.
Ключевая возможность — ssl_preread
Модуль ngx_stream_ssl_preread_module позволяет читать SNI (Server Name Indication) из ClientHello пакета TLS без расшифровки трафика. Это даёт возможность маршрутизировать TCP-соединения на разные бекенды в зависимости от доменного имени, которое запрашивает клиент.
Это НЕ то же самое, что http-проксирование
| Характеристика | http { } блок | stream { } блок |
|---|---|---|
| Уровень OSI | L7 (HTTP) | L4 (TCP/UDP) |
| Видит ли HTTP-заголовки | ✅ Да | ❌ Нет |
| Видит ли SNI | ✅ Да (после TLS) | ✅ Да (ssl_preread, без TLS) |
| Terminates TLS | ✅ Да | ❌ Нет (передаёт дальше) |
| Можно ли делать proxy_pass | ✅ Да | ✅ Да (TCP) |
2. Как это работает (схема)
│ SWAG Container │
│ │
Клиент ──── :443 ───► │ stream { │
(TLS ClientHello) │ ssl_preread on; ← читает SNI без decrypt │
│ map $ssl_preread_server_name $backend { │
│ nextcl.dev0ps.online → remnanode:8443 │
│ * (default) → 127.0.0.1:4443│
│ } │
│ proxy_pass $backend; │
│ } │
│ │ │ │
│ ▼ ▼ │
│ remnanode:8443 http { } :4443 │
│ (Reality/Xray, (SWAG, terminates │
│ TLS не трогаем) TLS, proxy_pass) │
│ │ │
│ ▼ │
│ site-confs/*.conf │
│ proxy-confs/*.conf │
└─────────────────────────────────────────────────┘
Поток данных
- Клиент подключается к порту 443
- stream блок принимает соединение и читает SNI из TLS ClientHello
- map выбирает бекенд на основе SNI:
- nextcl.dev0ps.online → remnanode:8443 (проксирует TCP как есть, TLS не трогается)
- всё остальное → 127.0.0.1:4443 (передаёт в http-блок SWAG)
- http блок на порту 4443 терминирует TLS и делает HTTP-проксирование
3. Какие проблемы решает
Проблема 1: Несколько TLS-сервисов на одном порту 443
Без stream — порт 443 занят SWAG. Reality/Xray или другой TLS-сервис не может слушать тот же порт.
Решение: stream маршрутизирует по SNI — разные домены идут на разные бекенды, все через один порт 443.
Проблема 2: Reality/Xray нужен необработанный TLS
Reality протокол (Xray/VLESS) требует, чтобы TLS-соединение дошло до бекенда без терминации. SWAG не может его обработать — он терминирует TLS.
Решение: stream проксирует TCP-соединение как есть, не расшифровывая. Reality получает оригинальный TLS-трафик.
Проблема 3: Нужен один IP для всего
На одном сервере работают и веб-сайты (SWAG), и VPN-протоколы (Reality). Оба требуют порт 443.
Решение: stream — единая точка входа на 443, маршрутизация по SNI.
4. Файловая структура — что куда класть
├── nginx.conf ← ГЛАВНЫЙ конфиг: добавить stream { } блок
├── stream.conf ← НОВЫЙ ФАЙЛ: SNI-роутинг (L4)
├── ssl.conf ← Без изменений
├── site-confs/
│ ├── default.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
│ └── dev0ps.online.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
└── proxy-confs/
└── *.subdomain.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
Порядок действий
| Шаг | Файл | Действие |
|---|---|---|
| 1 | nginx.conf | Добавить stream { include /config/nginx/stream.conf; } |
| 2 | stream.conf | Создать с SNI-роутингом |
| 3 | default.conf | Заменить listen 443 ssl → listen 4443 ssl |
| 4 | site-confs/*.conf | Заменить listen 443 ssl → listen 4443 ssl |
| 5 | proxy-confs/*.conf | Заменить listen 443 ssl → listen 4443 ssl |
5. Шаг 1: nginx.conf — добавить блок stream
Файл: /config/nginx/nginx.conf
Добавить блок stream { } после закрывающей скобки http { } и перед daemon off;:
http {
# ... (существующая конфигурация http без изменений) ...
# Важно: все server{} внутри http должны слушать 4443, НЕ 443!
include /etc/nginx/http.d/*.conf;
include /config/nginx/site-confs/*.conf;
}
daemon off;
pid /run/nginx.pid;
# ═══════════════════════════════════════════════════════
# STREAM BLOCK — SNI-роутинг на L4 уровне
# ═══════════════════════════════════════════════════════
stream {
include /config/nginx/stream.conf;
}
⚠️ Критически важно
- stream { } должен быть на верхнем уровне (не внутри http { }!)
- stream { } и http { } — это параллельные блоки одного уровня
- Внутри stream { } нельзя использовать http-директивы (server_name, location, proxy_set_header и т.д.)
6. Шаг 2: stream.conf — SNI-роутинг
Файл: /config/nginx/stream.conf (создать новый)
✅ ПРАВИЛЬНЫЙ конфиг (боевой)
resolver 127.0.0.11 valid=30s;
# SNI → backend mapping
# Адреса резолвятся через resolver ПРИ КАЖДОМ ЗАПРОСЕ,
# а не при загрузке конфига — nginx стартует даже если backend недоступен.
map $ssl_preread_server_name $backend {
nextcl.dev0ps.online remnanode:8443;
www.nextcl.dev0ps.online remnanode:8443;
default 127.0.0.1:4443;
}
server {
listen 443;
listen [::]:443;
ssl_preread on; # Читать SNI из TLS ClientHello без расшифровки
proxy_socket_keepalive on;
proxy_connect_timeout 10s;
proxy_timeout 3600s;
proxy_pass $backend;
}
❌ НЕПРАВИЛЬНО — upstream с доменным именем
upstream reality_backend {
server remnanode:8443; # ← ОШИБКА: DNS резолвится при загрузке конфига
}
map $ssl_preread_server_name $backend {
default reality_backend;
}
Почему upstream ломает всё
В stream { } блоке upstream с доменным именем (не IP) заставляет nginx резолвить DNS немедленно при загрузке конфигурации. Если контейнер не запущен:
→ nginx не стартует → ВСЕ сайты недоступны
Почему map + resolver работает
Когда proxy_pass получает значение через переменную ($backend), nginx использует resolver для DNS-разрешения во время запроса, а не при загрузке. Если бекенд недоступен — только этот маршрут не работает, остальные сайты продолжают работать.
7. Шаг 3: default.conf — перевести http на порт 4443
Файл: /config/nginx/site-confs/default.conf
Было (стандартный SWAG)
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
# ...
}
Стало (с stream)
listen 4443 ssl default_server;
listen [::]:4443 ssl default_server;
# ...
}
Полная замена: везде в default.conf где 443 → заменить на 4443.
Порт 80 оставить без изменений (HTTP → HTTPS редирект).
8. Шаг 4: site-confs — все server слушают 4443
Файлы: /config/nginx/site-confs/*.conf и /config/nginx/proxy-confs/*.conf
Было
listen 443 ssl;
server_name portainer.dev0ps.online;
# ...
}
Стало
listen 4443 ssl;
server_name portainer.dev0ps.online;
# ...
}
Массовая замена: во ВСЕХ файлах заменить listen 443 ssl → listen 4443 ssl
и listen [::]:443 ssl → listen [::]:4443 ssl
Порт 80 не трогать!
9. Боевой пример конфигурации
nginx.conf (фрагменты)
include /config/nginx/worker_processes.conf;
pcre_jit on;
error_log /config/log/nginx/error.log;
include /etc/nginx/modules/*.conf;
include /etc/nginx/conf.d/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /config/nginx/resolver.conf;
server_tokens off;
client_max_body_size 0;
sendfile on;
tcp_nopush on;
gzip_vary on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
http2 on;
http3 on;
quic_retry on;
access_log /config/log/nginx/access.log;
client_body_temp_path /tmp/nginx 1 2;
proxy_temp_path /tmp/nginx-proxy;
fastcgi_temp_path /tmp/nginx-fastcgi;
uwsgi_temp_path /tmp/nginx-uwsgi;
scgi_temp_path /tmp/nginx-scgi;
proxy_cache_path /tmp/nginx-proxy-cache keys_zone=lsio-proxy:10m;
fastcgi_cache_path /tmp/nginx-fcgi-cache keys_zone=lsio-fcgi:10m;
scgi_cache_path /tmp/nginx-scgi-cache keys_zone=lsio-scgi:10m;
uwsgi_cache_path /tmp/nginx-uwsgi-cache keys_zone=lsio-uwsgi:10m;
include /etc/nginx/http.d/*.conf;
include /config/nginx/site-confs/*.conf;
}
daemon off;
pid /run/nginx.pid;
stream {
include /config/nginx/stream.conf;
}
stream.conf (боевой)
map $ssl_preread_server_name $backend {
nextcl.dev0ps.online remnanode:8443;
www.nextcl.dev0ps.online remnanode:8443;
default 127.0.0.1:4443;
}
server {
listen 443;
listen [::]:443;
ssl_preread on;
proxy_socket_keepalive on;
proxy_connect_timeout 10s;
proxy_timeout 3600s;
proxy_pass $backend;
}
default.conf (боевой, фрагменты)
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
return 301 https://$host$request_uri;
}
}
# Основной HTTPS сервер (443 → 4443)
server {
listen 4443 ssl default_server;
listen [::]:4443 ssl default_server;
server_name _;
include /config/nginx/ssl.conf;
root /config/www;
index index.html index.htm index.php;
include /config/nginx/proxy-confs/*.subfolder.conf;
location / {
try_files $uri $uri/ /index.html /index.htm /index.php$is_args$args;
}
# ... PHP и остальные location без изменений ...
}
# Subdomain конфиги
include /config/nginx/proxy-confs/*.subdomain.conf;
Пример site-conf (dev0ps.online.conf, фрагмент)
server {
listen 80;
listen [::]:80;
server_name *.dev0ps.online;
return 301 https://$host$request_uri;
}
# Portainer — слушает 4443
server {
listen 4443 ssl;
server_name portainer.dev0ps.online;
include /config/nginx/ssl.conf;
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
location / {
proxy_pass https://192.168.1.203:9443;
include /config/nginx/proxy.conf;
}
}
# Proxmox — слушает 4443
server {
listen 4443 ssl;
server_name pdm.dev0ps.online;
include /config/nginx/ssl.conf;
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
location / {
proxy_pass https://192.168.1.92:8443;
include /config/nginx/proxy.conf;
proxy_ssl_verify off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
10. Проверка и отладка
Проверить синтаксис конфигурации
Ожидаемый вывод:
nginx: configuration file /etc/nginx/nginx.conf test is successful
Перезагрузить конфигурацию без перезапуска
Проверить, что nginx слушает правильные порты
Ожидаемый вывод:
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",...)) ← stream
LISTEN 0 511 0.0.0.0:4443 0.0.0.0:* users:(("nginx",...)) ← http
Проверить HTTPS через http-блок
# Ожидается: 200 или 301
Проверить SNI-роутинг извне
curl -sk https://portainer.dev0ps.online/
# Проверить, что Reality-домен резолвится
openssl s_client -connect YOUR_SERVER_IP:443 -servername nextcl.dev0ps.online
Логи
docker exec swag tail -50 /config/log/nginx/error.log
# Логи контейнера
docker logs swag --tail 50
11. Типичные ошибки и решения
❌ host not found in upstream
Причина: Используется upstream { server hostname:port; } — nginx резолвит DNS при загрузке.
Решение: Убрать upstream, использовать map с прямыми адресами + resolver:
upstream reality_backend {
server remnanode:8443;
}
# ТАК:
map $ssl_preread_server_name $backend {
nextcl.dev0ps.online remnanode:8443;
default 127.0.0.1:4443;
}
❌ bind() to 0.0.0.0:443 failed
Причина: Порт 443 уже занят — либо http-блок всё ещё слушает 443, либо другой процесс.
Решение: Убедиться, что в http-блоке НЕТ listen 443 — только listen 4443:
# Не должно быть listen 443 ssl (только 4443)
❌ duplicate map variable
Причина: map $http_upgrade $connection_upgrade определён дважды — в nginx.conf и в подключаемом файле (например, nextcl.subdomain.conf).
Решение: Удалить дублирующийся map из подключаемого файла, оставить только в nginx.conf.
❌ Сайты открываются, но SNI-роутинг не работает
Причина: ssl_preread on; не указан в stream server блоке.
Решение: Добавить ssl_preread on; в server { } внутри stream { }.
❌ 502 Bad Gateway для всех сайтов
Причина: stream проксирует на 127.0.0.1:4443, но http-блок не слушает 4443.
Решение: Проверить, что в default.conf и site-confs стоит listen 4443 ssl.
12. Docker Compose — порты
SWAG должен пробрасывать порт 443 из хоста:
swag:
image: lscr.io/linuxserver/swag
container_name: swag
ports:
- "80:80" # HTTP
- "81:81" # SWAG Dashboard
- "443:443" # HTTPS/TLS → stream блок
volumes:
- ./swag:/config
# ...
Порт 4443 НЕ пробрасывается наружу! Он используется только внутри контейнера для связи stream → http.
Краткая шпаргалка
docker exec swag nginx -t
# Перечитать конфиг
docker exec swag nginx -s reload
# Найти все места где слушается 443 (должно быть только в stream.conf)
grep -rn "listen.*443" /config/nginx/
# Проверить порты внутри контейнера
docker exec swag ss -tlnp
# Логи
docker logs swag --tail 30
docker exec swag tail -30 /config/log/nginx/error.log