0 Голосов

Stream (SNI-роутинг) в SWAG

Версия 1.1 от Anton Krivchenkov на 27.05.2026 18:05

📘 Гайд: Stream (SNI-роутинг) в SWAG — полная документация

Актуально для: SWAG (linuxserver/docker-swag) + nginx 1.25+
Последнее обновление: 2026-05-27


📑 Содержание

  1. Что такое stream и зачем он нужен
  2. Как это работает (схема)
  3. Какие проблемы решает
  4. Файловая структура — что куда класть
  5. Шаг 1: nginx.conf — добавить блок stream
  6. Шаг 2: stream.conf — SNI-роутинг
  7. Шаг 3: default.conf — перевести http на порт 4443
  8. Шаг 4: site-confs — все server слушают 4443
  9. Боевой пример конфигурации
  10. Проверка и отладка
  11. Типичные ошибки и решения
  12. 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 { } блок
Уровень OSIL7 (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           │
                        └─────────────────────────────────────────────────┘

Поток данных

  1. Клиент подключается к порту 443
  2. stream блок принимает соединение и читает SNI из TLS ClientHello
  3. map выбирает бекенд на основе SNI:
    • nextcl.dev0ps.onlineremnanode:8443 (проксирует TCP как есть, TLS не трогается)
    • всё остальное → 127.0.0.1:4443 (передаёт в http-блок SWAG)
  4. 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. Файловая структура — что куда класть

swag/nginx/
├── 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

Порядок действий

ШагФайлДействие
1nginx.confДобавить stream { include /config/nginx/stream.conf; }
2stream.confСоздать с SNI-роутингом
3default.confЗаменить listen 443 ssllisten 4443 ssl
4site-confs/*.confЗаменить listen 443 ssllisten 4443 ssl
5proxy-confs/*.confЗаменить listen 443 ssllisten 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 (создать новый)

✅ ПРАВИЛЬНЫЙ конфиг (боевой)

# DNS-резолвер Docker (для динамического разрешения имён контейнеров)
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 с доменным именем

# ТАК НЕ ДЕЛАТЬ! Если remnanode недоступен при старте — nginx УПАДЁТ
upstream reality_backend {
   server remnanode:8443;  # ← ОШИБКА: DNS резолвится при загрузке конфига
}

map $ssl_preread_server_name $backend {
   default reality_backend;
}

Почему upstream ломает всё

В stream { } блоке upstream с доменным именем (не IP) заставляет nginx резолвить DNS немедленно при загрузке конфигурации. Если контейнер не запущен:

nginx: [emerg] host not found in upstream "remnanode:8443" in /config/nginx/stream.conf:4

nginx не стартуетВСЕ сайты недоступны

Почему map + resolver работает

Когда proxy_pass получает значение через переменную ($backend), nginx использует resolver для DNS-разрешения во время запроса, а не при загрузке. Если бекенд недоступен — только этот маршрут не работает, остальные сайты продолжают работать.


7. Шаг 3: default.conf — перевести http на порт 4443

Файл: /config/nginx/site-confs/default.conf

Было (стандартный SWAG)

server {
   listen 443 ssl default_server;
   listen [::]:443 ssl default_server;
   # ...
}

Стало (с stream)

server {
   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

Было

server {
   listen 443 ssl;
   server_name portainer.dev0ps.online;
   # ...
}

Стало

server {
   listen 4443 ssl;
   server_name portainer.dev0ps.online;
   # ...
}

Массовая замена: во ВСЕХ файлах заменить listen 443 ssllisten 4443 ssl
и listen [::]:443 ssllisten [::]:4443 ssl
Порт 80 не трогать!


9. Боевой пример конфигурации

nginx.conf (фрагменты)

user abc;
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 (боевой)

resolver 127.0.0.11 valid=30s;

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 (боевой, фрагменты)

# HTTP → HTTPS redirect (порт 80 БЕЗ ИЗМЕНЕНИЙ)
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, фрагмент)

# HTTP → HTTPS redirect
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. Проверка и отладка

Проверить синтаксис конфигурации

docker exec swag nginx -t

Ожидаемый вывод:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Перезагрузить конфигурацию без перезапуска

docker exec swag nginx -s reload

Проверить, что nginx слушает правильные порты

docker exec swag ss -tlnp | grep nginx

Ожидаемый вывод:

LISTEN  0  511  0.0.0.0:80     0.0.0.0:*  users:(("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-блок

docker exec swag curl -sk -o /dev/null -w "%{http_code}" https://127.0.0.1:4443/
# Ожидается: 200 или 301

Проверить SNI-роутинг извне

# Проверить, что обычный сайт работает
curl -sk https://portainer.dev0ps.online/

# Проверить, что Reality-домен резолвится
openssl s_client -connect YOUR_SERVER_IP:443 -servername nextcl.dev0ps.online

Логи

# Логи ошибок nginx
docker exec swag tail -50 /config/log/nginx/error.log

# Логи контейнера
docker logs swag --tail 50

11. Типичные ошибки и решения

host not found in upstream

nginx: [emerg] host not found in upstream "remnanode:8443" in /config/nginx/stream.conf:4

Причина: Используется 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

nginx: [emerg] bind() to 0.0.0.0:443 failed (98: Address already in use)

Причина: Порт 443 уже занят — либо http-блок всё ещё слушает 443, либо другой процесс.

Решение: Убедиться, что в http-блоке НЕТ listen 443 — только listen 4443:

grep -rn "listen.*443" /config/nginx/site-confs/ /config/nginx/proxy-confs/
# Не должно быть listen 443 ssl (только 4443)

duplicate map variable

nginx: [emerg] duplicate variable "connection_upgrade"

Причина: 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 из хоста:

services:
 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