Исходный код вики Stream (SNI-роутинг) в SWAG
Редактировал(а) Anton Krivchenkov 27.05.2026 18:05
Скрыть последних авторов
| author | version | line-number | content |
|---|---|---|---|
![]() |
1.1 | 1 | # 📘 Гайд: Stream (SNI-роутинг) в SWAG — полная документация |
| 2 | |||
![]() |
2.1 | 3 | > **Актуально для:** SWAG (linuxserver/docker-swag) + nginx 1.25+ |
| 4 | > | ||
![]() |
1.1 | 5 | > **Последнее обновление:** 2026-05-27 |
| 6 | |||
| 7 | --- | ||
| 8 | |||
| 9 | ## 📑 Содержание | ||
| 10 | |||
| 11 | 1. [Что такое stream и зачем он нужен](#1-что-такое-stream-и-зачем-он-нужен) | ||
![]() |
2.1 | 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--порты) | ||
![]() |
1.1 | 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 | |||
![]() |
2.1 | 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) | | ||
![]() |
1.1 | 43 | |
| 44 | --- | ||
| 45 | |||
| 46 | ## 2. Как это работает (схема) | ||
| 47 | |||
![]() |
2.1 | 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 | └─────────────────────────────────────────────────┘ | ||
![]() |
1.1 | 69 | |
| 70 | ### Поток данных | ||
| 71 | |||
| 72 | 1. **Клиент** подключается к порту **443** | ||
![]() |
2.1 | 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-проксирование | ||
![]() |
1.1 | 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 | |||
![]() |
2.1 | 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 | ||
![]() |
1.1 | 114 | |
| 115 | ### Порядок действий | ||
| 116 | |||
![]() |
2.1 | 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` | | ||
![]() |
1.1 | 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 | |||
![]() |
2.1 | 157 | * `stream { }` должен быть **на верхнем уровне** (не внутри `http { }`!) |
| 158 | * `stream { }` и `http { }` — это **параллельные** блоки одного уровня | ||
| 159 | * Внутри `stream { }` **нельзя** использовать http-директивы (`server_name`, `location`, `proxy_set_header` и т.д.) | ||
![]() |
1.1 | 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 | |||
![]() |
2.1 | 248 | > **Полная замена:** везде в default.conf где `443` → заменить на `4443`. |
| 249 | > | ||
![]() |
1.1 | 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 | |||
![]() |
2.1 | 278 | > **Массовая замена:** во ВСЕХ файлах заменить `listen 443 ssl` → `listen 4443 ssl` |
| 279 | > | ||
| 280 | > и `listen [::]:443 ssl` → `listen [::]:4443 ssl` | ||
| 281 | > | ||
![]() |
1.1 | 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 | **Ожидаемый вывод:** | ||
![]() |
2.1 | 456 | |
![]() |
1.1 | 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 | **Ожидаемый вывод:** | ||
![]() |
2.1 | 475 | |
![]() |
1.1 | 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 | ``` |
