0 Голосов

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

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

Последние авторы
1 # 📘 Гайд: Stream (SNI-роутинг) в SWAG — полная документация
2
3 > **Актуально для:** SWAG (linuxserver/docker-swag) + nginx 1.25+
4 > **Последнее обновление:** 2026-05-27
5
6 ---
7
8 ## 📑 Содержание
9
10 1. [Что такое stream и зачем он нужен](#1-что-такое-stream-и-зачем-он-нужен)
11 2. [Как это работает (схема)](#2-как-это-работает-схема)
12 3. [Какие проблемы решает](#3-какие-проблемы-решает)
13 4. [Файловая структура — что куда класть](#4-файловая-структура--что-куда-класть)
14 5. [Шаг 1: nginx.conf — добавить блок stream](#5-шаг-1-nginxconf--добавить-блок-stream)
15 6. [Шаг 2: stream.conf — SNI-роутинг](#6-шаг-2-streamconf--sni-роутинг)
16 7. [Шаг 3: default.conf — перевести http на порт 4443](#7-шаг-3-defaultconf--перевести-http-на-порт-4443)
17 8. [Шаг 4: site-confs — все server слушают 4443](#8-шаг-4-site-confs--все-server-слушают-4443)
18 9. [Боевой пример конфигурации](#9-боевой-пример-конфигурации)
19 10. [Проверка и отладка](#10-проверка-и-отладка)
20 11. [Типичные ошибки и решения](#11-типичные-ошибки-и-решения)
21 12. [Docker Compose — порты](#12-docker-compose--порты)
22
23 ---
24
25 ## 1. Что такое stream и зачем он нужен
26
27 **Stream** — это модуль nginx для работы на **L4 (TCP/UDP)** уровне, в отличие от стандартного `http` блока, который работает на **L7 (HTTP)** уровне.
28
29 ### Ключевая возможность — `ssl_preread`
30
31 Модуль `ngx_stream_ssl_preread_module` позволяет читать **SNI (Server Name Indication)** из ClientHello пакета TLS **без расшифровки трафика**. Это даёт возможность маршрутизировать TCP-соединения на разные бекенды в зависимости от доменного имени, которое запрашивает клиент.
32
33 ### Это НЕ то же самое, что http-проксирование
34
35 | Характеристика | `http { }` блок | `stream { }` блок |
36 |---|---|---|
37 | Уровень OSI | L7 (HTTP) | L4 (TCP/UDP) |
38 | Видит ли HTTP-заголовки | ✅ Да | ❌ Нет |
39 | Видит ли SNI | ✅ Да (после TLS) | ✅ Да (ssl_preread, без TLS) |
40 | Terminates TLS | ✅ Да | ❌ Нет (передаёт дальше) |
41 | Можно ли делать proxy_pass | ✅ Да | ✅ Да (TCP) |
42
43 ---
44
45 ## 2. Как это работает (схема)
46
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
73 1. **Клиент** подключается к порту **443**
74 2. **stream** блок принимает соединение и читает SNI из TLS ClientHello
75 3. **map** выбирает бекенд на основе SNI:
76 - `nextcl.dev0ps.online` → `remnanode:8443` (проксирует TCP как есть, TLS не трогается)
77 - всё остальное → `127.0.0.1:4443` (передаёт в http-блок SWAG)
78 4. **http** блок на порту **4443** терминирует TLS и делает HTTP-проксирование
79
80 ---
81
82 ## 3. Какие проблемы решает
83
84 ### Проблема 1: Несколько TLS-сервисов на одном порту 443
85
86 Без stream — порт 443 занят SWAG. Reality/Xray или другой TLS-сервис не может слушать тот же порт.
87
88 **Решение:** stream маршрутизирует по SNI — разные домены идут на разные бекенды, все через один порт 443.
89
90 ### Проблема 2: Reality/Xray нужен необработанный TLS
91
92 Reality протокол (Xray/VLESS) требует, чтобы TLS-соединение дошло до бекенда **без терминации**. SWAG не может его обработать — он терминирует TLS.
93
94 **Решение:** stream проксирует TCP-соединение как есть, не расшифровывая. Reality получает оригинальный TLS-трафик.
95
96 ### Проблема 3: Нужен один IP для всего
97
98 На одном сервере работают и веб-сайты (SWAG), и VPN-протоколы (Reality). Оба требуют порт 443.
99
100 **Решение:** stream — единая точка входа на 443, маршрутизация по SNI.
101
102 ---
103
104 ## 4. Файловая структура — что куда класть
105
106 ```
107 swag/nginx/
108 ├── nginx.conf ← ГЛАВНЫЙ конфиг: добавить stream { } блок
109 ├── stream.conf ← НОВЫЙ ФАЙЛ: SNI-роутинг (L4)
110 ├── ssl.conf ← Без изменений
111 ├── site-confs/
112 │ ├── default.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
113 │ └── dev0ps.online.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
114 └── proxy-confs/
115 └── *.subdomain.conf ← ИЗМЕНИТЬ: listen 443 → listen 4443
116 ```
117
118 ### Порядок действий
119
120 | Шаг | Файл | Действие |
121 |-----|------|----------|
122 | 1 | `nginx.conf` | Добавить `stream { include /config/nginx/stream.conf; }` |
123 | 2 | `stream.conf` | Создать с SNI-роутингом |
124 | 3 | `default.conf` | Заменить `listen 443 ssl` → `listen 4443 ssl` |
125 | 4 | `site-confs/*.conf` | Заменить `listen 443 ssl` → `listen 4443 ssl` |
126 | 5 | `proxy-confs/*.conf` | Заменить `listen 443 ssl` → `listen 4443 ssl` |
127
128 ---
129
130 ## 5. Шаг 1: nginx.conf — добавить блок stream
131
132 **Файл:** `/config/nginx/nginx.conf`
133
134 Добавить блок `stream { }` **после закрывающей скобки `http { }`** и перед `daemon off;`:
135
136 ```nginx
137 # ... (начало файла без изменений) ...
138
139 http {
140 # ... (существующая конфигурация http без изменений) ...
141
142 # Важно: все server{} внутри http должны слушать 4443, НЕ 443!
143 include /etc/nginx/http.d/*.conf;
144 include /config/nginx/site-confs/*.conf;
145 }
146
147 daemon off;
148 pid /run/nginx.pid;
149
150 # ═══════════════════════════════════════════════════════
151 # STREAM BLOCK — SNI-роутинг на L4 уровне
152 # ═══════════════════════════════════════════════════════
153 stream {
154 include /config/nginx/stream.conf;
155 }
156 ```
157
158 ### ⚠️ Критически важно
159
160 - `stream { }` должен быть **на верхнем уровне** (не внутри `http { }`!)
161 - `stream { }` и `http { }` — это **параллельные** блоки одного уровня
162 - Внутри `stream { }` **нельзя** использовать http-директивы (`server_name`, `location`, `proxy_set_header` и т.д.)
163
164 ---
165
166 ## 6. Шаг 2: stream.conf — SNI-роутинг
167
168 **Файл:** `/config/nginx/stream.conf` (создать новый)
169
170 ### ✅ ПРАВИЛЬНЫЙ конфиг (боевой)
171
172 ```nginx
173 # DNS-резолвер Docker (для динамического разрешения имён контейнеров)
174 resolver 127.0.0.11 valid=30s;
175
176 # SNI → backend mapping
177 # Адреса резолвятся через resolver ПРИ КАЖДОМ ЗАПРОСЕ,
178 # а не при загрузке конфига — nginx стартует даже если backend недоступен.
179 map $ssl_preread_server_name $backend {
180 nextcl.dev0ps.online remnanode:8443;
181 www.nextcl.dev0ps.online remnanode:8443;
182 default 127.0.0.1:4443;
183 }
184
185 server {
186 listen 443;
187 listen [::]:443;
188
189 ssl_preread on; # Читать SNI из TLS ClientHello без расшифровки
190 proxy_socket_keepalive on;
191 proxy_connect_timeout 10s;
192 proxy_timeout 3600s;
193
194 proxy_pass $backend;
195 }
196 ```
197
198 ### ❌ НЕПРАВИЛЬНО — upstream с доменным именем
199
200 ```nginx
201 # ТАК НЕ ДЕЛАТЬ! Если remnanode недоступен при старте — nginx УПАДЁТ
202 upstream reality_backend {
203 server remnanode:8443; # ← ОШИБКА: DNS резолвится при загрузке конфига
204 }
205
206 map $ssl_preread_server_name $backend {
207 default reality_backend;
208 }
209 ```
210
211 ### Почему upstream ломает всё
212
213 В `stream { }` блоке `upstream` с доменным именем (не IP) заставляет nginx резолвить DNS **немедленно при загрузке конфигурации**. Если контейнер не запущен:
214
215 ```
216 nginx: [emerg] host not found in upstream "remnanode:8443" in /config/nginx/stream.conf:4
217 ```
218
219 → **nginx не стартует** → **ВСЕ сайты недоступны**
220
221 ### Почему map + resolver работает
222
223 Когда `proxy_pass` получает значение через **переменную** (`$backend`), nginx использует `resolver` для DNS-разрешения **во время запроса**, а не при загрузке. Если бекенд недоступен — только этот маршрут не работает, остальные сайты продолжают работать.
224
225 ---
226
227 ## 7. Шаг 3: default.conf — перевести http на порт 4443
228
229 **Файл:** `/config/nginx/site-confs/default.conf`
230
231 ### Было (стандартный SWAG)
232
233 ```nginx
234 server {
235 listen 443 ssl default_server;
236 listen [::]:443 ssl default_server;
237 # ...
238 }
239 ```
240
241 ### Стало (с stream)
242
243 ```nginx
244 server {
245 listen 4443 ssl default_server;
246 listen [::]:4443 ssl default_server;
247 # ...
248 }
249 ```
250
251 > **Полная замена:** везде в default.conf где `443` → заменить на `4443`.
252 > Порт 80 оставить без изменений (HTTP → HTTPS редирект).
253
254 ---
255
256 ## 8. Шаг 4: site-confs — все server слушают 4443
257
258 **Файлы:** `/config/nginx/site-confs/*.conf` и `/config/nginx/proxy-confs/*.conf`
259
260 ### Было
261
262 ```nginx
263 server {
264 listen 443 ssl;
265 server_name portainer.dev0ps.online;
266 # ...
267 }
268 ```
269
270 ### Стало
271
272 ```nginx
273 server {
274 listen 4443 ssl;
275 server_name portainer.dev0ps.online;
276 # ...
277 }
278 ```
279
280 > **Массовая замена:** во ВСЕХ файлах заменить `listen 443 ssl` → `listen 4443 ssl`
281 > и `listen [::]:443 ssl` → `listen [::]:4443 ssl`
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 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
458 nginx: configuration file /etc/nginx/nginx.conf test is successful
459 ```
460
461 ### Перезагрузить конфигурацию без перезапуска
462
463 ```bash
464 docker exec swag nginx -s reload
465 ```
466
467 ### Проверить, что nginx слушает правильные порты
468
469 ```bash
470 docker exec swag ss -tlnp | grep nginx
471 ```
472
473 **Ожидаемый вывод:**
474 ```
475 LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",...))
476 LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",...)) ← stream
477 LISTEN 0 511 0.0.0.0:4443 0.0.0.0:* users:(("nginx",...)) ← http
478 ```
479
480 ### Проверить HTTPS через http-блок
481
482 ```bash
483 docker exec swag curl -sk -o /dev/null -w "%{http_code}" https://127.0.0.1:4443/
484 # Ожидается: 200 или 301
485 ```
486
487 ### Проверить SNI-роутинг извне
488
489 ```bash
490 # Проверить, что обычный сайт работает
491 curl -sk https://portainer.dev0ps.online/
492
493 # Проверить, что Reality-домен резолвится
494 openssl s_client -connect YOUR_SERVER_IP:443 -servername nextcl.dev0ps.online
495 ```
496
497 ### Логи
498
499 ```bash
500 # Логи ошибок nginx
501 docker exec swag tail -50 /config/log/nginx/error.log
502
503 # Логи контейнера
504 docker logs swag --tail 50
505 ```
506
507 ---
508
509 ## 11. Типичные ошибки и решения
510
511 ### ❌ `host not found in upstream`
512
513 ```
514 nginx: [emerg] host not found in upstream "remnanode:8443" in /config/nginx/stream.conf:4
515 ```
516
517 **Причина:** Используется `upstream { server hostname:port; }` — nginx резолвит DNS при загрузке.
518
519 **Решение:** Убрать `upstream`, использовать `map` с прямыми адресами + `resolver`:
520
521 ```nginx
522 # НЕ ТАК:
523 upstream reality_backend {
524 server remnanode:8443;
525 }
526
527 # ТАК:
528 map $ssl_preread_server_name $backend {
529 nextcl.dev0ps.online remnanode:8443;
530 default 127.0.0.1:4443;
531 }
532 ```
533
534 ### ❌ `bind() to 0.0.0.0:443 failed`
535
536 ```
537 nginx: [emerg] bind() to 0.0.0.0:443 failed (98: Address already in use)
538 ```
539
540 **Причина:** Порт 443 уже занят — либо http-блок всё ещё слушает 443, либо другой процесс.
541
542 **Решение:** Убедиться, что в http-блоке НЕТ `listen 443` — только `listen 4443`:
543
544 ```bash
545 grep -rn "listen.*443" /config/nginx/site-confs/ /config/nginx/proxy-confs/
546 # Не должно быть listen 443 ssl (только 4443)
547 ```
548
549 ### ❌ `duplicate map variable`
550
551 ```
552 nginx: [emerg] duplicate variable "connection_upgrade"
553 ```
554
555 **Причина:** `map $http_upgrade $connection_upgrade` определён дважды — в `nginx.conf` и в подключаемом файле (например, `nextcl.subdomain.conf`).
556
557 **Решение:** Удалить дублирующийся `map` из подключаемого файла, оставить только в `nginx.conf`.
558
559 ### ❌ Сайты открываются, но SNI-роутинг не работает
560
561 **Причина:** `ssl_preread on;` не указан в stream server блоке.
562
563 **Решение:** Добавить `ssl_preread on;` в `server { }` внутри `stream { }`.
564
565 ### ❌ 502 Bad Gateway для всех сайтов
566
567 **Причина:** stream проксирует на `127.0.0.1:4443`, но http-блок не слушает 4443.
568
569 **Решение:** Проверить, что в `default.conf` и `site-confs` стоит `listen 4443 ssl`.
570
571 ---
572
573 ## 12. Docker Compose — порты
574
575 SWAG должен пробрасывать порт **443** из хоста:
576
577 ```yaml
578 services:
579 swag:
580 image: lscr.io/linuxserver/swag
581 container_name: swag
582 ports:
583 - "80:80" # HTTP
584 - "81:81" # SWAG Dashboard
585 - "443:443" # HTTPS/TLS → stream блок
586 volumes:
587 - ./swag:/config
588 # ...
589 ```
590
591 > **Порт 4443 НЕ пробрасывается наружу!** Он используется только внутри контейнера для связи `stream → http`.
592
593 ---
594
595 ## Краткая шпаргалка
596
597 ```bash
598 # Проверить конфиг
599 docker exec swag nginx -t
600
601 # Перечитать конфиг
602 docker exec swag nginx -s reload
603
604 # Найти все места где слушается 443 (должно быть только в stream.conf)
605 grep -rn "listen.*443" /config/nginx/
606
607 # Проверить порты внутри контейнера
608 docker exec swag ss -tlnp
609
610 # Логи
611 docker logs swag --tail 30
612 docker exec swag tail -30 /config/log/nginx/error.log
613 ```