0 Голосов

Исходный код вики Stream (SNI-роутинг) в SWAG

Редактировал(а) Anton Krivchenkov 27.05.2026 18:05

Последние авторы
1 # 📘 Гайд: Stream (SNI-роутинг) в SWAG — полная документация
2
3 > **Актуально для:** SWAG (linuxserver/docker-swag) + nginx 1.25+
4 >
5 > **Последнее обновление:** 2026-05-27
6
7 ---
8
9 ## 📑 Содержание
10
11 1. [Что такое stream и зачем он нужен](#1-что-такое-stream-и-зачем-он-нужен)
12 1. [Как это работает (схема)](#2-как-это-работает-схема)
13 1. [Какие проблемы решает](#3-какие-проблемы-решает)
14 1. [Файловая структура — что куда класть](#4-файловая-структура--что-куда-класть)
15 1. [Шаг 1: nginx.conf — добавить блок stream](#5-шаг-1-nginxconf--добавить-блок-stream)
16 1. [Шаг 2: stream.conf — SNI-роутинг](#6-шаг-2-streamconf--sni-роутинг)
17 1. [Шаг 3: default.conf — перевести http на порт 4443](#7-шаг-3-defaultconf--перевести-http-на-порт-4443)
18 1. [Шаг 4: site-confs — все server слушают 4443](#8-шаг-4-site-confs--все-server-слушают-4443)
19 1. [Боевой пример конфигурации](#9-боевой-пример-конфигурации)
20 1. [Проверка и отладка](#10-проверка-и-отладка)
21 1. [Типичные ошибки и решения](#11-типичные-ошибки-и-решения)
22 1. [Docker Compose — порты](#12-docker-compose--порты)
23
24 ---
25
26 ## 1. Что такое stream и зачем он нужен
27
28 **Stream** — это модуль nginx для работы на **L4 (TCP/UDP)** уровне, в отличие от стандартного `http` блока, который работает на **L7 (HTTP)** уровне.
29
30 ### Ключевая возможность — `ssl_preread`
31
32 Модуль `ngx_stream_ssl_preread_module` позволяет читать **SNI (Server Name Indication)** из ClientHello пакета TLS **без расшифровки трафика**. Это даёт возможность маршрутизировать TCP-соединения на разные бекенды в зависимости от доменного имени, которое запрашивает клиент.
33
34 ### Это НЕ то же самое, что http-проксирование
35
36 | Характеристика | `http { }` блок | `stream { }` блок |
37 | -------------------------- | ---------------- | --------------------------- |
38 | Уровень OSI | L7 (HTTP) | L4 (TCP/UDP) |
39 | Видит ли HTTP-заголовки | ✅ Да | ❌ Нет |
40 | Видит ли SNI | ✅ Да (после TLS) | ✅ Да (ssl_preread, без TLS) |
41 | Terminates TLS | ✅ Да | ❌ Нет (передаёт дальше) |
42 | Можно ли делать proxy_pass | ✅ Да | ✅ Да (TCP) |
43
44 ---
45
46 ## 2. Как это работает (схема)
47
48 ┌─────────────────────────────────────────────────┐
49 │ SWAG Container │
50 │ │
51 Клиент ──── :443 ───► │ stream { │
52 (TLS ClientHello) │ ssl_preread on; ← читает SNI без decrypt │
53 │ map $ssl_preread_server_name $backend { │
54 │ nextcl.dev0ps.online → remnanode:8443 │
55 │ * (default) → 127.0.0.1:4443│
56 │ } │
57 │ proxy_pass $backend; │
58 │ } │
59 │ │ │ │
60 │ ▼ ▼ │
61 │ remnanode:8443 http { } :4443 │
62 │ (Reality/Xray, (SWAG, terminates │
63 │ TLS не трогаем) TLS, proxy_pass) │
64 │ │ │
65 │ ▼ │
66 │ site-confs/*.conf │
67 │ proxy-confs/*.conf │
68 └─────────────────────────────────────────────────┘
69
70 ### Поток данных
71
72 1. **Клиент** подключается к порту **443**
73 1. **stream** блок принимает соединение и читает SNI из TLS ClientHello
74 1. **map** выбирает бекенд на основе SNI:
75 * `nextcl.dev0ps.online` → `remnanode:8443` (проксирует TCP как есть, TLS не трогается)
76 * всё остальное → `127.0.0.1:4443` (передаёт в http-блок SWAG)
77 1. **http** блок на порту **4443** терминирует TLS и делает HTTP-проксирование
78
79 ---
80
81 ## 3. Какие проблемы решает
82
83 ### Проблема 1: Несколько TLS-сервисов на одном порту 443
84
85 Без stream — порт 443 занят SWAG. Reality/Xray или другой TLS-сервис не может слушать тот же порт.
86
87 **Решение:** stream маршрутизирует по SNI — разные домены идут на разные бекенды, все через один порт 443.
88
89 ### Проблема 2: Reality/Xray нужен необработанный TLS
90
91 Reality протокол (Xray/VLESS) требует, чтобы TLS-соединение дошло до бекенда **без терминации**. SWAG не может его обработать — он терминирует TLS.
92
93 **Решение:** stream проксирует TCP-соединение как есть, не расшифровывая. Reality получает оригинальный TLS-трафик.
94
95 ### Проблема 3: Нужен один IP для всего
96
97 На одном сервере работают и веб-сайты (SWAG), и VPN-протоколы (Reality). Оба требуют порт 443.
98
99 **Решение:** stream — единая точка входа на 443, маршрутизация по SNI.
100
101 ---
102
103 ## 4. Файловая структура — что куда класть
104
105 swag/nginx/
106 ├── nginx.conf ← ГЛАВНЫЙ конфиг: добавить stream { } блок
107 ├── stream.conf ← НОВЫЙ ФАЙЛ: SNI-роутинг (L4)
108 ├── ssl.conf ← Без изменений
109 ├── site-confs/
110 │ ├── default.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
111 │ └── dev0ps.online.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
112 └── proxy-confs/
113 └── *.subdomain.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
114
115 ### Порядок действий
116
117 | Шаг | Файл | Действие |
118 | --- | -------------------- | -------------------------------------------------------- |
119 | 1 | `nginx.conf` | Добавить `stream { include /config/nginx/stream.conf; }` |
120 | 2 | `stream.conf` | Создать с SNI-роутингом |
121 | 3 | `default.conf` | Заменить `listen 443 ssl` → `listen 4443 ssl` |
122 | 4 | `site-confs/*.conf` | Заменить `listen 443 ssl` → `listen 4443 ssl` |
123 | 5 | `proxy-confs/*.conf` | Заменить `listen 443 ssl` → `listen 4443 ssl` |
124
125 ---
126
127 ## 5. Шаг 1: nginx.conf — добавить блок stream
128
129 **Файл:** `/config/nginx/nginx.conf`
130
131 Добавить блок `stream { }` **после закрывающей скобки `http { }`** и перед `daemon off;`:
132
133 ```nginx
134 # ... (начало файла без изменений) ...
135
136 http {
137 # ... (существующая конфигурация http без изменений) ...
138
139 # Важно: все server{} внутри http должны слушать 4443, НЕ 443!
140 include /etc/nginx/http.d/*.conf;
141 include /config/nginx/site-confs/*.conf;
142 }
143
144 daemon off;
145 pid /run/nginx.pid;
146
147 # ═══════════════════════════════════════════════════════
148 # STREAM BLOCK — SNI-роутинг на L4 уровне
149 # ═══════════════════════════════════════════════════════
150 stream {
151 include /config/nginx/stream.conf;
152 }
153 ```
154
155 ### ⚠️ Критически важно
156
157 * `stream { }` должен быть **на верхнем уровне** (не внутри `http { }`!)
158 * `stream { }` и `http { }` — это **параллельные** блоки одного уровня
159 * Внутри `stream { }` **нельзя** использовать http-директивы (`server_name`, `location`, `proxy_set_header` и т.д.)
160
161 ---
162
163 ## 6. Шаг 2: stream.conf — SNI-роутинг
164
165 **Файл:** `/config/nginx/stream.conf` (создать новый)
166
167 ### ✅ ПРАВИЛЬНЫЙ конфиг (боевой)
168
169 ```nginx
170 # DNS-резолвер Docker (для динамического разрешения имён контейнеров)
171 resolver 127.0.0.11 valid=30s;
172
173 # SNI → backend mapping
174 # Адреса резолвятся через resolver ПРИ КАЖДОМ ЗАПРОСЕ,
175 # а не при загрузке конфига — nginx стартует даже если backend недоступен.
176 map $ssl_preread_server_name $backend {
177 nextcl.dev0ps.online remnanode:8443;
178 www.nextcl.dev0ps.online remnanode:8443;
179 default 127.0.0.1:4443;
180 }
181
182 server {
183 listen 443;
184 listen [::]:443;
185
186 ssl_preread on; # Читать SNI из TLS ClientHello без расшифровки
187 proxy_socket_keepalive on;
188 proxy_connect_timeout 10s;
189 proxy_timeout 3600s;
190
191 proxy_pass $backend;
192 }
193 ```
194
195 ### ❌ НЕПРАВИЛЬНО — upstream с доменным именем
196
197 ```nginx
198 # ТАК НЕ ДЕЛАТЬ! Если remnanode недоступен при старте — nginx УПАДЁТ
199 upstream reality_backend {
200 server remnanode:8443; # ← ОШИБКА: DNS резолвится при загрузке конфига
201 }
202
203 map $ssl_preread_server_name $backend {
204 default reality_backend;
205 }
206 ```
207
208 ### Почему upstream ломает всё
209
210 В `stream { }` блоке `upstream` с доменным именем (не IP) заставляет nginx резолвить DNS **немедленно при загрузке конфигурации**. Если контейнер не запущен:
211
212 ```
213 nginx: [emerg] host not found in upstream "remnanode:8443" in /config/nginx/stream.conf:4
214 ```
215
216 → **nginx не стартует** → **ВСЕ сайты недоступны**
217
218 ### Почему map + resolver работает
219
220 Когда `proxy_pass` получает значение через **переменную** (`$backend`), nginx использует `resolver` для DNS-разрешения **во время запроса**, а не при загрузке. Если бекенд недоступен — только этот маршрут не работает, остальные сайты продолжают работать.
221
222 ---
223
224 ## 7. Шаг 3: default.conf — перевести http на порт 4443
225
226 **Файл:** `/config/nginx/site-confs/default.conf`
227
228 ### Было (стандартный SWAG)
229
230 ```nginx
231 server {
232 listen 443 ssl default_server;
233 listen [::]:443 ssl default_server;
234 # ...
235 }
236 ```
237
238 ### Стало (с stream)
239
240 ```nginx
241 server {
242 listen 4443 ssl default_server;
243 listen [::]:4443 ssl default_server;
244 # ...
245 }
246 ```
247
248 > **Полная замена:** везде в default.conf где `443` → заменить на `4443`.
249 >
250 > Порт 80 оставить без изменений (HTTP → HTTPS редирект).
251
252 ---
253
254 ## 8. Шаг 4: site-confs — все server слушают 4443
255
256 **Файлы:** `/config/nginx/site-confs/*.conf` и `/config/nginx/proxy-confs/*.conf`
257
258 ### Было
259
260 ```nginx
261 server {
262 listen 443 ssl;
263 server_name portainer.dev0ps.online;
264 # ...
265 }
266 ```
267
268 ### Стало
269
270 ```nginx
271 server {
272 listen 4443 ssl;
273 server_name portainer.dev0ps.online;
274 # ...
275 }
276 ```
277
278 > **Массовая замена:** во ВСЕХ файлах заменить `listen 443 ssl` → `listen 4443 ssl`
279 >
280 > и `listen [::]:443 ssl` → `listen [::]:4443 ssl`
281 >
282 > Порт 80 не трогать!
283
284 ---
285
286 ## 9. Боевой пример конфигурации
287
288 ### nginx.conf (фрагменты)
289
290 ```nginx
291 user abc;
292 include /config/nginx/worker_processes.conf;
293 pcre_jit on;
294 error_log /config/log/nginx/error.log;
295 include /etc/nginx/modules/*.conf;
296 include /etc/nginx/conf.d/*.conf;
297
298 events {
299 worker_connections 1024;
300 }
301
302 http {
303 include /etc/nginx/mime.types;
304 default_type application/octet-stream;
305 include /config/nginx/resolver.conf;
306 server_tokens off;
307 client_max_body_size 0;
308 sendfile on;
309 tcp_nopush on;
310 gzip_vary on;
311
312 map $http_upgrade $connection_upgrade {
313 default upgrade;
314 '' close;
315 }
316
317 http2 on;
318 http3 on;
319 quic_retry on;
320
321 access_log /config/log/nginx/access.log;
322 client_body_temp_path /tmp/nginx 1 2;
323 proxy_temp_path /tmp/nginx-proxy;
324 fastcgi_temp_path /tmp/nginx-fastcgi;
325 uwsgi_temp_path /tmp/nginx-uwsgi;
326 scgi_temp_path /tmp/nginx-scgi;
327
328 proxy_cache_path /tmp/nginx-proxy-cache keys_zone=lsio-proxy:10m;
329 fastcgi_cache_path /tmp/nginx-fcgi-cache keys_zone=lsio-fcgi:10m;
330 scgi_cache_path /tmp/nginx-scgi-cache keys_zone=lsio-scgi:10m;
331 uwsgi_cache_path /tmp/nginx-uwsgi-cache keys_zone=lsio-uwsgi:10m;
332
333 include /etc/nginx/http.d/*.conf;
334 include /config/nginx/site-confs/*.conf;
335 }
336
337 daemon off;
338 pid /run/nginx.pid;
339
340 stream {
341 include /config/nginx/stream.conf;
342 }
343 ```
344
345 ### stream.conf (боевой)
346
347 ```nginx
348 resolver 127.0.0.11 valid=30s;
349
350 map $ssl_preread_server_name $backend {
351 nextcl.dev0ps.online remnanode:8443;
352 www.nextcl.dev0ps.online remnanode:8443;
353 default 127.0.0.1:4443;
354 }
355
356 server {
357 listen 443;
358 listen [::]:443;
359
360 ssl_preread on;
361 proxy_socket_keepalive on;
362 proxy_connect_timeout 10s;
363 proxy_timeout 3600s;
364
365 proxy_pass $backend;
366 }
367 ```
368
369 ### default.conf (боевой, фрагменты)
370
371 ```nginx
372 # HTTP → HTTPS redirect (порт 80 БЕЗ ИЗМЕНЕНИЙ)
373 server {
374 listen 80 default_server;
375 listen [::]:80 default_server;
376 location / {
377 return 301 https://$host$request_uri;
378 }
379 }
380
381 # Основной HTTPS сервер (443 → 4443)
382 server {
383 listen 4443 ssl default_server;
384 listen [::]:4443 ssl default_server;
385
386 server_name _;
387 include /config/nginx/ssl.conf;
388 root /config/www;
389 index index.html index.htm index.php;
390
391 include /config/nginx/proxy-confs/*.subfolder.conf;
392
393 location / {
394 try_files $uri $uri/ /index.html /index.htm /index.php$is_args$args;
395 }
396
397 # ... PHP и остальные location без изменений ...
398 }
399
400 # Subdomain конфиги
401 include /config/nginx/proxy-confs/*.subdomain.conf;
402 ```
403
404 ### Пример site-conf (dev0ps.online.conf, фрагмент)
405
406 ```nginx
407 # HTTP → HTTPS redirect
408 server {
409 listen 80;
410 listen [::]:80;
411 server_name *.dev0ps.online;
412 return 301 https://$host$request_uri;
413 }
414
415 # Portainer — слушает 4443
416 server {
417 listen 4443 ssl;
418 server_name portainer.dev0ps.online;
419 include /config/nginx/ssl.conf;
420 add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
421 location / {
422 proxy_pass https://192.168.1.203:9443;
423 include /config/nginx/proxy.conf;
424 }
425 }
426
427 # Proxmox — слушает 4443
428 server {
429 listen 4443 ssl;
430 server_name pdm.dev0ps.online;
431 include /config/nginx/ssl.conf;
432 add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
433 location / {
434 proxy_pass https://192.168.1.92:8443;
435 include /config/nginx/proxy.conf;
436 proxy_ssl_verify off;
437 proxy_set_header Host $host;
438 proxy_set_header X-Real-IP $remote_addr;
439 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
440 proxy_set_header X-Forwarded-Proto $scheme;
441 }
442 }
443 ```
444
445 ---
446
447 ## 10. Проверка и отладка
448
449 ### Проверить синтаксис конфигурации
450
451 ```bash
452 docker exec swag nginx -t
453 ```
454
455 **Ожидаемый вывод:**
456
457 ```
458 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
459 nginx: configuration file /etc/nginx/nginx.conf test is successful
460 ```
461
462 ### Перезагрузить конфигурацию без перезапуска
463
464 ```bash
465 docker exec swag nginx -s reload
466 ```
467
468 ### Проверить, что nginx слушает правильные порты
469
470 ```bash
471 docker exec swag ss -tlnp | grep nginx
472 ```
473
474 **Ожидаемый вывод:**
475
476 ```
477 LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",...))
478 LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",...)) ← stream
479 LISTEN 0 511 0.0.0.0:4443 0.0.0.0:* users:(("nginx",...)) ← http
480 ```
481
482 ### Проверить HTTPS через http-блок
483
484 ```bash
485 docker exec swag curl -sk -o /dev/null -w "%{http_code}" https://127.0.0.1:4443/
486 # Ожидается: 200 или 301
487 ```
488
489 ### Проверить SNI-роутинг извне
490
491 ```bash
492 # Проверить, что обычный сайт работает
493 curl -sk https://portainer.dev0ps.online/
494
495 # Проверить, что Reality-домен резолвится
496 openssl s_client -connect YOUR_SERVER_IP:443 -servername nextcl.dev0ps.online
497 ```
498
499 ### Логи
500
501 ```bash
502 # Логи ошибок nginx
503 docker exec swag tail -50 /config/log/nginx/error.log
504
505 # Логи контейнера
506 docker logs swag --tail 50
507 ```
508
509 ---
510
511 ## 11. Типичные ошибки и решения
512
513 ### ❌ `host not found in upstream`
514
515 ```
516 nginx: [emerg] host not found in upstream "remnanode:8443" in /config/nginx/stream.conf:4
517 ```
518
519 **Причина:** Используется `upstream { server hostname:port; }` — nginx резолвит DNS при загрузке.
520
521 **Решение:** Убрать `upstream`, использовать `map` с прямыми адресами + `resolver`:
522
523 ```nginx
524 # НЕ ТАК:
525 upstream reality_backend {
526 server remnanode:8443;
527 }
528
529 # ТАК:
530 map $ssl_preread_server_name $backend {
531 nextcl.dev0ps.online remnanode:8443;
532 default 127.0.0.1:4443;
533 }
534 ```
535
536 ### ❌ `bind() to 0.0.0.0:443 failed`
537
538 ```
539 nginx: [emerg] bind() to 0.0.0.0:443 failed (98: Address already in use)
540 ```
541
542 **Причина:** Порт 443 уже занят — либо http-блок всё ещё слушает 443, либо другой процесс.
543
544 **Решение:** Убедиться, что в http-блоке НЕТ `listen 443` — только `listen 4443`:
545
546 ```bash
547 grep -rn "listen.*443" /config/nginx/site-confs/ /config/nginx/proxy-confs/
548 # Не должно быть listen 443 ssl (только 4443)
549 ```
550
551 ### ❌ `duplicate map variable`
552
553 ```
554 nginx: [emerg] duplicate variable "connection_upgrade"
555 ```
556
557 **Причина:** `map $http_upgrade $connection_upgrade` определён дважды — в `nginx.conf` и в подключаемом файле (например, `nextcl.subdomain.conf`).
558
559 **Решение:** Удалить дублирующийся `map` из подключаемого файла, оставить только в `nginx.conf`.
560
561 ### ❌ Сайты открываются, но SNI-роутинг не работает
562
563 **Причина:** `ssl_preread on;` не указан в stream server блоке.
564
565 **Решение:** Добавить `ssl_preread on;` в `server { }` внутри `stream { }`.
566
567 ### ❌ 502 Bad Gateway для всех сайтов
568
569 **Причина:** stream проксирует на `127.0.0.1:4443`, но http-блок не слушает 4443.
570
571 **Решение:** Проверить, что в `default.conf` и `site-confs` стоит `listen 4443 ssl`.
572
573 ---
574
575 ## 12. Docker Compose — порты
576
577 SWAG должен пробрасывать порт **443** из хоста:
578
579 ```yaml
580 services:
581 swag:
582 image: lscr.io/linuxserver/swag
583 container_name: swag
584 ports:
585 - "80:80" # HTTP
586 - "81:81" # SWAG Dashboard
587 - "443:443" # HTTPS/TLS → stream блок
588 volumes:
589 - ./swag:/config
590 # ...
591 ```
592
593 > **Порт 4443 НЕ пробрасывается наружу!** Он используется только внутри контейнера для связи `stream → http`.
594
595 ---
596
597 ## Краткая шпаргалка
598
599 ```bash
600 # Проверить конфиг
601 docker exec swag nginx -t
602
603 # Перечитать конфиг
604 docker exec swag nginx -s reload
605
606 # Найти все места где слушается 443 (должно быть только в stream.conf)
607 grep -rn "listen.*443" /config/nginx/
608
609 # Проверить порты внутри контейнера
610 docker exec swag ss -tlnp
611
612 # Логи
613 docker logs swag --tail 30
614 docker exec swag tail -30 /config/log/nginx/error.log
615 ```