Stream-модуль: проксирование TCP и UDP

Рейтинг: 75.8% · 12 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

Stream-модуль: проксирование TCP и UDP

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP (вы здесь)
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
До этого урока ты гонял через nginx только HTTP и HTTPS. Но в реальной инфраструктуре за одним входом живёт зоопарк сервисов, которые про HTTP вообще ничего не знают: PostgreSQL на 5432, Redis на 6379, RabbitMQ, SMTP, DNS, syslog, игровой сервер на UDP, SSH. И вот приходит задача - вынести реплики базы за единую точку входа с балансировкой, или раскидать TLS-трафик на 443 по разным бэкендам в зависимости от домена, не расшифровывая его. HTTP-блок тут бессилен: он парсит запрос как HTTP, а у тебя бинарный протокол PostgreSQL. Решение - stream-модуль: nginx работает как L4-прокси, тупо перекладывая байты TCP или датаграммы UDP между клиентом и бэкендом, не понимая и не трогая содержимое.

Это и есть nginx stream - отдельный контекст

Код: Выделить всё

stream {}
на верхнем уровне конфига, брат-близнец . Внутри свои директивы, свои переменные, свои upstream. В этом уроке разберём механику L4-проксирования, балансировку TCP, тонкости UDP, роутинг по SNI через ssl_preread, передачу реального IP через proxy_protocol и грабли, на которые наступают все.

Контекст stream {} - это не про tcp_nopush из http

Сразу убираем главную путаницу, на которой спотыкаются даже опытные люди. В эталонном проекте ngx-trace-gateway есть файл globals/global_tcp.conf со строками

Код: Выделить всё

sendfile on; tcp_nopush on; tcp_nodelay on;
. Это не stream-модуль. Эти директивы живут внутри и настраивают поведение TCP-сокетов для HTTP-соединений (как nginx пишет ответ в сокет: копит ли пакеты, шлёт ли мелкие куски сразу). Слово tcp в названии файла сбивает с толку, но к L4-проксированию произвольного TCP это отношения не имеет.

Настоящий stream - это отдельный блок верхнего уровня, рядом с http, а не внутри него:

Код: Выделить всё

# nginx.conf
user  nginx;
worker_processes  auto;

events { worker_connections 10240; }

http {
    # ... весь твой HTTP-мир: server {}, location {}, proxy_pass на http://...
}

stream {
    # ... совершенно другой мир: TCP/UDP L4-проксирование
    upstream postgres_backend {
        server 10.0.0.11:5432 max_fails=2 fail_timeout=10s;
        server 10.0.0.12:5432 max_fails=2 fail_timeout=10s;
    }

    server {
        listen 5432;
        proxy_pass postgres_backend;
    }
}
Ключевая разница в модели. В http nginx парсит запрос: видит метод, URI, заголовки, выбирает server по Host, location по пути, может переписать, закэшировать, добавить заголовки. В stream нет ни location, ни URI, ни заголовков - есть только сокет и поток байт. Server в stream выбирается исключительно по паре listen (адрес:порт) и, для TLS, по SNI через preread. Никаких

Код: Выделить всё

location
внутри stream-server не бывает - это синтаксическая ошибка. Запомни: stream работает на 4-м уровне модели OSI (транспорт), http - на 7-м (приложение).

Изображение

nginx tcp proxy: балансировка реплик базы и очередей

Самый частый сценарий - nginx tcp балансировка для не-HTTP сервисов. Реплики PostgreSQL, кластер Redis, ноды RabbitMQ, пул SMTP-релеев. Клиент коннектится на один адрес nginx, а тот раскидывает соединения по живым бэкендам. Соберём боевой конфиг для read-реплик базы:

Код: Выделить всё

stream {
    log_format stream_log '$remote_addr [$time_local] '
                          '$protocol $status $bytes_sent $bytes_received '
                          '$session_time "$upstream_addr" '
                          '$upstream_connect_time $upstream_bytes_sent';

    upstream db_read_replicas {
        least_conn;                 # метод балансировки для долгих соединений
        zone db_read 64k;           # shared-зона для состояния пиров между воркерами
        server 10.0.0.21:5432 weight=2 max_fails=3 fail_timeout=30s;
        server 10.0.0.22:5432        max_fails=3 fail_timeout=30s;
        server 10.0.0.23:5432        max_fails=3 fail_timeout=30s backup;
    }

    server {
        listen 5432;
        proxy_pass         db_read_replicas;

        proxy_connect_timeout 3s;   # сколько ждём установки коннекта к бэкенду
        proxy_timeout         30m;  # таймаут БЕЗДЕЙСТВИЯ соединения (не всей сессии!)
        proxy_next_upstream   on;   # пробовать следующий пир при ошибке коннекта
        proxy_next_upstream_tries 2;

        access_log /var/log/nginx/db_stream.log stream_log;
    }
}
Разберём по косточкам. Методы балансировки в stream те же, что в http, и объявляются так же: по умолчанию round-robin, есть

Код: Выделить всё

least_conn
(отдаёт соединение пиру с наименьшим числом активных - идеально для БД, где коннекты живут долго и неравномерно),

Код: Выделить всё

hash $remote_addr consistent
(липкость по IP клиента, чтобы один клиент всегда попадал на одну ноду - это и есть нативные sticky-сессии на L4),

Код: Выделить всё

random two least_conn
. Директива , кстати, в stream не существует - используй

Код: Выделить всё

hash $remote_addr
, это её аналог.

proxy_timeout - главная грабля новичка. Это не лимит длительности сессии, а таймаут простоя: если по соединению в обе стороны N времени не идёт байтов, nginx его рвёт. Для БД с длинными idle-транзакциями ставь минуты, иначе клиент будет получать внезапные обрывы соединения. По умолчанию там 10 минут.

Health checks: в open-source nginx активных проверок здоровья для stream нет - только пассивные через

Код: Выделить всё

max_fails
/

Код: Выделить всё

fail_timeout
(если N коннектов подряд упали, пир выводится на fail_timeout). Это значит, что мёртвую реплику nginx заметит только когда сам словит ошибку на живом клиентском трафике. Если нужны настоящие активные health checks (nginx сам периодически стучится в порт) плюс slow_start (плавный ввод поднявшейся ноды под нагрузку) - это есть бесплатно в Angie через директиву

Код: Выделить всё

health_check
в stream, либо в nginx Plus. Это весомый аргумент в пользу Angie именно для stream-балансировки баз.

nginx udp: DNS, syslog и почему это сложнее

TCP - это установленное соединение с понятным жизненным циклом. nginx udp - совсем другая история, потому что UDP не имеет соединений вообще: это разрозненные датаграммы без состояния. nginx эмулирует "сессию" по паре адрес+порт клиента, но это искусственная конструкция. Балансируем DNS-резолверы:

Код: Выделить всё

stream {
    upstream dns_servers {
        server 10.0.0.31:53;
        server 10.0.0.32:53;
    }

    server {
        listen 53 udp;              # ОБЯЗАТЕЛЕН параметр udp
        proxy_pass     dns_servers;
        proxy_responses 1;          # ждём ровно 1 датаграмму-ответ, потом закрываем сессию
        proxy_timeout   3s;         # короткий таймаут - DNS быстрый
    }

    # А вот syslog - односторонний, ответа НЕТ:
    server {
        listen 514 udp;
        proxy_pass     log_collectors;
        proxy_responses 0;          # ответов не ждём вообще, сессия закрывается сразу
    }
}
Ключевая директива - proxy_responses. Она говорит nginx, сколько ответных датаграмм ожидать на одну клиентскую, и работает как подсказка для завершения UDP-сессии. Для DNS-запроса ответ ровно один - ставим 1, и nginx закроет сессию сразу после ответа, не держа её до таймаута. Для syslog ответа нет - ставим 0, сессия закрывается мгновенно после отправки. Если оставить значение по умолчанию (без числа - ждать неограниченно), nginx будет держать UDP-сессию до истечения

Код: Выделить всё

proxy_timeout
, зря расходуя память на тысячах клиентов.

Грабли UDP, которые надо знать заранее:
  • UDP stateless - "сессия" в nginx это фикция по адресу клиента. За NAT десятки клиентов могут слиться в одну сессию, если у них совпал внешний адрес+порт. Для тяжёлого UDP это реальная проблема.
  • Если протокол шлёт несколько датаграмм в ответ (а ты поставил proxy_responses 1), nginx закроет сессию после первой и потеряет остальные. Знай ответную семантику своего протокола.
  • Балансировка UDP по умолчанию round-robin раскидывает каждую датаграмму потенциально на разный бэкенд. Для stateful-протоколов поверх UDP (тот же QUIC, игровые серверы) это ломает всё - нужен

    Код: Выделить всё

    hash $remote_addr
    , чтобы прибить клиента к одному бэкенду.
  • Параметр

    Код: Выделить всё

    reuseport
    на listen раскидывает UDP-датаграммы по воркерам ядром, снимая узкое место единого сокета - для высоконагруженного UDP почти обязателен.
nginx ssl_preread: SNI-роутинг на 443 без расшифровки TLS

Теперь самое мощное. Представь: на одном внешнем IP и порту 443 у тебя несколько разных бэкендов, и каждый сам владеет своим TLS-сертификатом. Ты не хочешь терминировать TLS на nginx (private key должен остаться на бэкенде - например, по требованиям безопасности, или это чужой managed-сервис). Как раскидать трафик по домену, если он зашифрован?

Ответ - nginx ssl_preread. Модуль ngx_stream_ssl_preread читает самое начало TLS-рукопожатия - сообщение ClientHello, которое клиент шлёт открытым текстом до шифрования. В ClientHello лежит расширение SNI (Server Name Indication) с запрашиваемым доменом. nginx подсматривает имя, не расшифровывая ничего, и роутит соединение целиком на нужный бэкенд. End-to-end TLS сохраняется: nginx вообще не видит расшифрованный трафик, это TLS passthrough.

Код: Выделить всё

stream {
    map $ssl_preread_server_name $tls_upstream {
        api.example.com        api_pool;
        grafana.example.com    grafana_pool;
        ~^.*\.internal\.lan$   internal_pool;   # по regex-маске поддоменов
        default                fallback_pool;
    }

    upstream api_pool      { server 10.0.0.41:8443; }
    upstream grafana_pool  { server 10.0.0.42:3000; }
    upstream internal_pool { server 10.0.0.43:8443; }
    upstream fallback_pool { server 10.0.0.44:8443; }

    server {
        listen 443;
        ssl_preread on;            # включаем чтение ClientHello БЕЗ терминации
        proxy_pass  $tls_upstream; # бэкенд из map по SNI
        proxy_protocol on;         # отдадим бэкенду реальный IP клиента (см. ниже)
    }
}
Что тут происходит по шагам: клиент открывает TCP к nginx:443, шлёт ClientHello с SNI=grafana.example.com. nginx с

Код: Выделить всё

ssl_preread on
парсит ClientHello, заполняет переменную

Код: Выделить всё

$ssl_preread_server_name
, map превращает её в имя upstream grafana_pool, и

Код: Выделить всё

proxy_pass $tls_upstream
перекладывает байты на бэкенд. TLS-рукопожатие завершается между клиентом и бэкендом, nginx лишь прокачивает шифр туда-сюда. Кроме SNI модуль умеет читать

Код: Выделить всё

$ssl_preread_protocol
(версия TLS) и

Код: Выделить всё

$ssl_preread_alpn_protocols
(список ALPN - например, отличить h2 от http/1.1 или раскидать по протоколу).

Важное ограничение: ssl_preread видит только то, что в открытом ClientHello. Если клиент использует ECH (Encrypted Client Hello, шифрование SNI - появляется массово в 2026), то имя домена внутри зашифровано, и preread его не достанет. Это by design ECH - спрятать SNI от промежуточных узлов. Учитывай при проектировании passthrough-роутинга.

nginx proxy_protocol: реальный IP клиента через L4

Вот фундаментальная проблема L4-проксирования. Когда nginx проксирует TCP, бэкенд видит в качестве источника соединения адрес nginx, а не клиента - на уровне TCP так и должно быть, соединение-то новое. В HTTP мы решаем это заголовком

Код: Выделить всё

X-Forwarded-For
. Но в stream нет заголовков - это голый TCP. Как тогда отдать бэкенду реальный IP?

Ответ - nginx proxy_protocol: бинарный (v2) или текстовый (v1) префикс, который nginx дописывает в самое начало TCP-соединения к бэкенду. В этом префиксе - настоящие адрес и порт клиента. Бэкенд (PostgreSQL с расширением, HAProxy, Envoy, другой nginx, многие приложения) парсит этот заголовок и узнаёт клиента. Это L4-аналог X-Forwarded-For.

Связь с realip работает в обе стороны. Исходящая - nginx добавляет proxy_protocol к бэкенду через

Код: Выделить всё

proxy_protocol on;
в stream-server (как в примере с ssl_preread выше). Входящая - если перед нашим nginx стоит балансировщик/CDN, который сам шлёт нам proxy_protocol, мы должны его прочитать и восстановить реальный IP клиента в логах и rate-limit:

Код: Выделить всё

stream {
    server {
        listen 5432 proxy_protocol;   # ПРИНИМАЕМ proxy_protocol от вышестоящего LB

        # модуль ngx_stream_realip восстанавливает $remote_addr из заголовка:
        set_real_ip_from 10.0.0.0/8;  # доверяем ТОЛЬКО известной сети LB
        set_real_ip_from 172.16.0.0/12;

        proxy_pass db_read_replicas;
        proxy_protocol on;            # и дальше пробрасываем настоящий IP бэкенду
    }
}
Это критично с точки зрения безопасности, ровно как и в HTTP-realip из прошлых уроков.

Код: Выделить всё

set_real_ip_from
должен доверять только известным сетям твоих балансировщиков. Если ты доверишь 0.0.0.0/0 или забудешь ограничить, любой клиент сможет подделать proxy_protocol-заголовок и притвориться любым IP - сломав твои логи, geo-фильтры, rate-limit по IP и любую авторизацию по адресу. Доверяй только своему LB.

Ещё одна мина: если ты поставил

Код: Выделить всё

listen ... proxy_protocol
, то nginx обязательно ждёт этот префикс в начале каждого соединения. Подключишься на этот порт обычным клиентом без proxy_protocol (telnet, psql напрямую) - соединение сразу оборвётся с ошибкой парсинга. Этот порт теперь только для тех, кто говорит на proxy_protocol.

Лимиты, логи и TLS-терминация в stream

Защита соединениями. В stream работает

Код: Выделить всё

limit_conn
- ограничение числа одновременных соединений с одного ключа. Для не-HTTP сервиса это часто единственная защита от исчерпания коннектов:

Код: Выделить всё

stream {
    limit_conn_zone $binary_remote_addr zone=perip:10m;

    server {
        listen 6379;                  # Redis
        limit_conn perip 10;          # не более 10 коннектов с одного IP
        proxy_pass redis_backend;
    }
}
TLS-терминация (в отличие от passthrough через ssl_preread) - когда хочешь, наоборот, расшифровать TLS на nginx и пустить на бэкенд уже plaintext. Тогда вешаешь

Код: Выделить всё

listen 5432 ssl;
и обычные

Код: Выделить всё

ssl_certificate
/

Код: Выделить всё

ssl_certificate_key
прямо в stream-server. Полезно, например, чтобы дать TLS базе, которая сама шифровать не умеет, или терминировать TLS перед Redis. Помни про современный TLS:

Код: Выделить всё

ssl_protocols TLSv1.2 TLSv1.3;
, а ciphers для TLS 1.3 задаются не через ssl_ciphers (они на 1.3 не действуют), а через

Код: Выделить всё

ssl_conf_command Ciphersuites ...
.

Логирование в stream обязательно настраивать вручную - дефолтного access_log тут нет, и без него ты слеп. Свой

Код: Выделить всё

log_format
со stream-переменными:

Код: Выделить всё

$protocol
(TCP/UDP), (статус сессии, не HTTP-код),

Код: Выделить всё

$bytes_sent
/

Код: Выделить всё

$bytes_received
,

Код: Выделить всё

$session_time
,

Код: Выделить всё

$upstream_addr
,

Код: Выделить всё

$upstream_connect_time
,

Код: Выделить всё

$ssl_preread_server_name
. HTTP-переменных вроде $request или $http_user_agent тут нет - запроса как такового не существует.

Мини-лаба: TCP-балансировка и SNI-роутинг руками

Повторяем по шагам на тестовой машине. Понадобятся nginx с модулями stream (в официальных пакетах и docker они уже собраны).
  • Шаг 1. Проверь, что stream-модуль есть:

    Код: Выделить всё

    nginx -V 2>&1 | tr ' ' '\n' | grep stream
    . Должны увидеть

    Код: Выделить всё

    --with-stream
    ,

    Код: Выделить всё

    --with-stream_ssl_preread_module
    ,

    Код: Выделить всё

    --with-stream_realip_module
    .
  • Шаг 2. Подними два простых TCP-бэкенда в разных терминалах:

    Код: Выделить всё

    ncat -lk 9001 --sh-exec 'echo BACKEND-1'
    и

    Код: Выделить всё

    ncat -lk 9002 --sh-exec 'echo BACKEND-2'
    .
  • Шаг 3. Создай stream-конфиг с upstream из 9001 и 9002 (round-robin) и

    Код: Выделить всё

    listen 9000;
    , как в примере с db_read_replicas, но порты 9001/9002.
  • Шаг 4. Проверь синтаксис и перечитай:

    Код: Выделить всё

    nginx -t && nginx -s reload
    . Помни - healthcheck в эталонном docker как раз и есть

    Код: Выделить всё

    nginx -t
    .
  • Шаг 5. Постучись несколько раз:

    Код: Выделить всё

    for i in 1 2 3 4; do ncat 127.0.0.1 9000; done
    . Увидишь, что ответы чередуются BACKEND-1 / BACKEND-2 - это round-robin в действии.
  • Шаг 6. Останови ncat на 9002 и снова постучись - после пары ошибок (max_fails) nginx выведет мёртвый пир и пойдёт только на живой. Это пассивный health check.
  • Шаг 7. Для SNI: добавь

    Код: Выделить всё

    map $ssl_preread_server_name
    ,

    Код: Выделить всё

    ssl_preread on;
    и

    Код: Выделить всё

    listen 8443;
    . Проверь роутинг:

    Код: Выделить всё

    openssl s_client -connect 127.0.0.1:8443 -servername api.example.com
    против

    Код: Выделить всё

    -servername grafana.example.com
    и убедись по access_log, что попал на разные upstream.
Контрольные вопросы
  • Чем директивы из global_tcp.conf эталона (sendfile, tcp_nopush) отличаются от контекста

    Код: Выделить всё

    stream {}
    ? Где живёт каждая?
  • Зачем нужен

    Код: Выделить всё

    proxy_responses
    в UDP-прокси и какое значение поставишь для DNS, а какое для одностороннего syslog?
  • Как ssl_preread роутит TLS-трафик по домену, ничего не расшифровывая, и почему ECH ломает этот подход?
  • Почему

    Код: Выделить всё

    set_real_ip_from
    нельзя открывать на 0.0.0.0/0 при приёме proxy_protocol? Что сможет сделать злоумышленник?
Итог

stream-модуль превращает nginx из HTTP-прокси в универсальный L4-шлюз для любого TCP и UDP. Запомни четыре опоры: это отдельный контекст stream {} (не путать с tcp_nopush из http), балансировка не-HTTP сервисов с пассивными health checks (активные - в Angie), UDP требует осторожности с proxy_responses и stateless-сессиями, а ssl_preread плюс proxy_protocol дают SNI-роутинг без расшифровки TLS и передачу реального IP клиента сквозь L4. Там, где за одним входом стоят базы, очереди, DNS и зашифрованные домены, stream незаменим - HTTP-блок тут просто не применим.
👍4 ❤️3 🔥 😄 🤔2
Аватара пользователя
regexops
Сообщения: 1
Зарегистрирован: 07 июн 2026, 08:45

Re: Stream-модуль: проксирование TCP и UDP

Сообщение regexops »

Год путал global_tcp.conf и stream, спасибо что разжевали - реально думал tcp_nopush это про tcp-проксирование. Теперь дошло что это два разных мира.
👍1 ❤️ 🔥1 😄 🤔
Аватара пользователя
rcmoncho
Сообщения: 1
Зарегистрирован: 06 июн 2026, 23:36

Re: Stream-модуль: проксирование TCP и UDP

Сообщение rcmoncho »

А можно через ssl_preread роутить по ALPN, а не по SNI? У меня h2 и grpc на одном порту 443 надо развести на разные бэкенды.
👍2 ❤️2 🔥 😄 🤔
Ответить
← Предыдущая глава
Динамические модули, njs и WAF
Следующая глава →
Деплой, перезагрузка и конфиг как код

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
Похожие запросы: njs и динамические модули nginxStream в nginx: проксирование TCP и UDP

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость