Привет, Хабр!

Сегодня разберём то, что обычно остаётся в уголке конфигов и редких комментов в тикетах. Sticky‑сессии в stream для RDP и SSH. В частности, как в Angie вытащить RDP‑cookie на preread‑стадии и привязать пользователя к одному backend'у без плясок с костылями в L4.

В реальных RDP‑фермах два симптома встречаются чаще всего. Первый — периодический разлёт сессий: пользователь переподключается и попадает на другой сервер, там у него пустой рабочий стол, злость, звонок в поддержку. Второй — попытки лечить это на уровне клиентского IP, которые ломаются при NAT и мобильных операторах. Решение простое: читать RDP‑cookie до принятия решения о балансировке и привязывать сессию к серверу. Angie это умеет из коробки модулем rdp_preread и sticky в stream‑upstream.

Теперь о том, что такое RDP‑cookie. На этапе X.224 Connection Request клиент может отправить строку, которая по спецификации выглядит как Cookie: mstshash=IDENTIFIER\r\n. Это идёт в чистом виде до TLS и может использоваться балансировщиком для привязки к сеансу. Если включён routingToken, cookie может отсутствовать. Также исторически часть клиентов обрезает идентификатор до 9 символов, а некоторые реализации отправляют username в этом поле. Это всё объясняет, почему на периметре мы регулярно видим «mstshash=Administr».

В Angie модуль RDP Preread делает как раз то, что нужно: на фазе preread читает начальные байты и раскрывает переменные $rdp_cookie и $rdp_cookie_<name>, например $rdp_cookie_mstshash. Включается одной директивой rdp_preread on; на уровне stream или server.

Дальше — sticky. В stream‑upstream есть два режима, необходимые для RDP‑кейсов. sticky route и sticky learn. В первом вы сами поставляете идентификатор маршрута и метите бэкенды sid=. Во втором Angie хранит соответствие «sessid → сервер» в общей зоне и автоматически научается возвращать клиента туда же. Для RDP нам чаще удобен learn с lookup=$rdp_cookie create=$rdp_cookie. Вариант с route хорош, когда сервер сам выдаёт «маршрут» и мы хотим жёстко на него садиться. Обе схемы поддерживают дополнительные параметры, включая строгий режим sticky_strict и секрет для хеширования sticky_secret.

Это отправная точка, на которой всё уже работает:

# /etc/angie/angie.conf (фрагмент)
stream {
    # читаем начальные байты, чтобы достать cookie
    rdp_preread on;                        # preread-стадия для RDP
    preread_buffer_size 16k;               # дефолт ок, увеличивайте при нестандартных клиентах
    preread_timeout 30s;                   # разумный таймаут на чтение прелюдии

    # ограничитель коннектов: ключ — сначала mstshash, иначе IP
    map $rdp_cookie_mstshash $limit_key {
        ~.+     $rdp_cookie_mstshash;
        default $binary_remote_addr;
    }

    limit_conn_zone $limit_key zone=rdp_limit:10m;  # общая зона для счётчиков

    upstream rdp_pool {
        zone rdp_pool_zone 256k;           # делайте зону всегда, это даёт метрики и max_conns
        # два RDP-хоста
        server 10.0.0.11:3389  max_conns=500 max_fails=2 fail_timeout=10s sid=a;
        server 10.0.0.12:3389  max_conns=500 max_fails=2 fail_timeout=10s sid=b;

        # сессии учатся и ищутся по RDP-cookie
        sticky learn lookup=$rdp_cookie create=$rdp_cookie zone=sessions:8m;
        # при необходимости защитить идентификаторы:
        # sticky_secret rdp.salt.$remote_addr;
    }

    server {
        listen 0.0.0.0:3389;
        proxy_connect_timeout 5s;
        proxy_timeout 1h;                  # RDP держит долгие TCP
        proxy_next_upstream on;            # при отказе — пробуем следующий бэкенд
        status_zone rdp_listener;          # метрики по этому серверу

        limit_conn rdp_limit 3;            # до 3 параллельных коннектов на пользователя

        # логируем аккуратно: маскируем cookie
        log_format rdp_json escape=json
            '{ "time":"$time_iso8601", "addr":"$remote_addr", '
            '"status":"$status", "proto":"$protocol", "sess":"$session_time", '
            '"upstream":"$upstream_addr", "mstshash":"$rdp_cookie_mstshash_masked" }';

        map $rdp_cookie_mstshash $rdp_cookie_mstshash_masked {
            ~^(.{3}).+$  "$1***";          # только первые 3 символа
            default      "-";
        }

        access_log /var/log/angie/rdp-stream.log rdp_json buffer=32k;

        proxy_pass rdp_pool;
    }
}

rdp_preread включён в верхнем уровне stream — значит переменные будут доступны внутри. Размер preread‑буфера и таймаут оставлены дефолтными; увеличивайте только если реально ловите клиенты, шлёпающие слишком большие заголовки на старте. Sticky настроен в learn‑режиме и кладёт соответствия в zone=sessions:8m. При отказе бэкенда proxy_next_upstream on; позволит быстро перепробовать следующий. В логах мы не записываем полный mstshash, только маску.

Разберём крайние случаи, которые всплывают в эксплуатации. Если клиент вместо mstshash прислал routingToken, $rdp_cookie будет пустым. В таком случае lookup ничего не найдёт, и Angie применит обычный метод балансировки, по умолчанию — round‑robin. Чтобы не возить RDP‑сессии между хостами, как fallback добавьте hash $remote_addr consistent; в upstream до sticky. Тогда при отсутствии cookie клиент стабильно попадёт по IP‑хешу на один и тот же бэкенд.

Теперь другой режим — когда вы заранее знаете маршруты и хотите жёстко маппить значение cookie на серверы через sid. Это нужно, если приложение или RDS‑фабрика возвратом cookie сама помечает сервер.

stream {
    rdp_preread on;

    # готовим route из mstshash (пример: первая буква user -> a/b)
    map $rdp_cookie_mstshash $route {
        ~^[a-mA-M]  "a";
        ~^[n-zN-Z]  "b";
        default     "";
    }

    upstream rdp_pool {
        zone rdp_pool_zone 256k;
        server 10.0.0.11:3389 sid=a;
        server 10.0.0.12:3389 sid=b;

        sticky route $route;
        # при желании соль
        # sticky_secret rdp.route.salt;
    }

    server {
        listen 3389;
        proxy_pass rdp_pool;
    }
}

В route‑режиме список переменных в sticky route читается по очереди до первой непустой. Значение сравнивается с sid= у серверов. Если соответствия нет — работает метод балансировки, заданный раньше.sticky надо объявлять после методов балансировки, иначе привязка не сработает.

Пару слов про SSH. У SSH нет cookie. Привязка строится либо на IP‑хеше, либо на первых байтах баннера при желании заморочиться с njs. В большинстве кейсов достаточно консистентного хеша по адресу клиента, вот так:

stream {
    upstream ssh_pool {
        zone ssh_zone 256k;
        hash $binary_remote_addr consistent;
        server 10.0.0.21:22 max_conns=200 fail_timeout=10s;
        server 10.0.0.22:22 max_conns=200 fail_timeout=10s;
    }

    server {
        listen 22;
        proxy_connect_timeout 3s;
        proxy_timeout 1h;
        limit_conn_zone $binary_remote_addr zone=ssh_limit:10m;
        limit_conn ssh_limit 5;

        access_log /var/log/angie/ssh-stream.log basic;
        log_format basic '$remote_addr [$time_local] '
                         '$protocol $status $bytes_sent $bytes_received '
                         '$session_time';

        proxy_pass ssh_pool;
    }
}

Если нужно извлечь часть баннера SSH для телеметрии или хитрой маршрутизации, смотрите stream‑js модуль и preread‑фазу. Но это отдельный, более тонкий сценарий. Для sticky достаточно hash $binary_remote_addr consistent.

Дальше — лимиты и защита. В stream‑модуле limit_conn умеет считать коннекты по произвольному ключу. В RDP‑кейсе логично ограничивать по mstshash, а при его отсутствии — по IP. Именно для этого в базовом примере я использовал map на $limit_key. Включайте limit_conn_zone на весь stream, а применяйте limit_conn в нужных server.

Таймауты и буферы. Для RDP оставляем proxy_timeout на час, иначе ловимнеожиданные обрывы при длительном простое. proxy_connect_timeout держите маленьким, чтобы быстро уходить к следующему бэкенду. Параметры preread не завышайте без нужды: переполненный preread_buffer_size приводит к закрытию соединения на ранней фазе.

Чтобы иметь в API статистику по stream‑upstream и по listener«ам, прописывайте zone у upstream и status_zone у серверов. В Angie статусный API умеет показывать метрики по stream, а также счётчики limit_conn. Это полезно для алертинга и для адекватной ёмкостной оценки. В последних версиях добавили поддержку переменных в status_zone, так что можно группировать по своим ключам.

Про отказоустойчивость. В open‑source‑версии у вас пассивные проверки по max_fails и fail_timeout, плюс proxy_next_upstream. В PRO доступен активный upstream_probe и «drain» у server для аккуратного вывода узла из ротации, при этом sticky‑сессии досиживают на своём сервере.

Для полноты картины приведу ещё один конфиг — гибрид с fallback«ом на hash и более жёсткими лимитами по пользователю. На нём удобно проходить нагрузочные тесты.

stream {
    rdp_preread on;

    # fallback-метод для тех клиентов, где cookie нет
    upstream rdp_pool {
        zone rdp_pool_zone 512k;

        # сначала стабильное распределение по IP
        hash $binary_remote_addr consistent;

        server 10.0.0.11:3389 max_conns=800 max_fails=2 fail_timeout=10s sid=a;
        server 10.0.0.12:3389 max_conns=800 max_fails=2 fail_timeout=10s sid=b;

        # sticky идёт после метода балансировки
        sticky learn lookup=$rdp_cookie create=$rdp_cookie zone=sessions:16m timeout=4h;
        # если хотите жёстко проваливать при отсутствии соответствия:
        # sticky_strict on;
    }

    # лимитируем более строго по mstshash
    map $rdp_cookie_mstshash $rdp_limit_key {
        ~.+     $rdp_cookie_mstshash;
        default $binary_remote_addr;
    }
    limit_conn_zone $rdp_limit_key zone=rdp_hard_limit:20m;

    server {
        listen 3389;
        status_zone rdp_listener_a;

        limit_conn rdp_hard_limit 2;
        proxy_connect_timeout 3s;
        proxy_timeout 2h;
        proxy_next_upstream on;

        # метрики резолвера тоже можно собрать
        resolver 127.0.0.53 status_zone=resolver_rdp;

        access_log /var/log/angie/rdp-access.json rdp_json buffer=64k gzip flush=5s;

        proxy_pass rdp_pool;
    }
}

И пример строки лога, чтобы ориентироваться глазами:

{ "time":"2025-09-25T12:34:56+00:00", "addr":"203.0.113.7",
  "status":"200", "proto":"TCP", "sess":"523.114",
  "upstream":"10.0.0.11:3389", "mstshash":"adm***" }

Для stream‑логов формат объявляется в блоке stream, а не http. Если вы вынесли формат не туда, Angie ругнётся «unknown log format». Это частая мелочь при миграциях. Для метрик по зонам в API обязательно указывайте zone в upstream и status_zone у серверов и резолвера. Проще всего снимать показания через HTTP‑подсистему с модулем prometheus, но базовые цифры есть и в API без сторонних экспортеров.

Наконец, контрольные списки перед выкладкой в прод:

  1. Придумайте ключи для ограничителей. Если пользователь авторизуется под доменной учёткой и у вас mstshash действительно отражает нужный идентификатор, лимитируйте по $rdp_cookie_mstshash. Если нет — по $binary_remote_addr. Комбинируйте через map.

  2. Всегда включайте zone у upstream и status_zone у listener«ов.

  3. Для RDP включайте rdp_preread on; именно там, где будет использоваться $rdp_cookie. Это может быть как весь stream, так и конкретный server. Следите за preread_buffer_size и preread_timeout.

  4. Выбирайте режим sticky осознанно. learn хорош как дефолт для RDP. route — когда заранее известны sid и вы хотите пружинящую, но контролируемую маршрутизацию. Не забудьте объявить sticky после метода балансировки.

  5. Не храните лишнее в логах. Маскируйте cookie. В access‑лог под stream это делается обычным map и log_format с escape=json.

  6. Если у вас Angie PRO, посмотрите на upstream_probe и drain.


Итого. sticky в stream для RDP делается через rdp_preread и sticky learn или route, для SSH достаточно hash по адресу, добавляем limit_conn по ключу mstshash или IP, аккуратные логи и status_zone для метрик, таймауты выставляем без фанатизма, fallback на hash держим на случай отсутствия cookie, в PRO смотрим probe и drain для плановых работ. Делитесь опытом в комментариях.

Если вы разобрались, как настроить sticky‑сессии для RDP и SSH, понимаете, как работает preread‑фаза, как корректно ограничивать подключения и вести метрики по stream‑upstream, следующий шаг — освоить весь набор инструментов, которые Angie и Nginx предоставляют для администрирования сетевых потоков.

На курсе «Администрирование Nginx/Angie» мы подробно разбираем, как строить устойчивые балансировщики, настраивать preread‑модули, sticky‑сессии, лимитировать пользователей по mstshash или IP, а также организовывать аккуратные логи и метрики.

Нужен системный рост без переплат? Подписка OTUS позволяет собрать трек из трёх курсов на 6 месяцев и менять их по ходу. Гибкость плюс экономия — редкое сочетание.

Комментарии (0)