Привет, Хабр!
Сегодня разберём то, что обычно остаётся в уголке конфигов и редких комментов в тикетах. 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 без сторонних экспортеров.
Наконец, контрольные списки перед выкладкой в прод:
Придумайте ключи для ограничителей. Если пользователь авторизуется под доменной учёткой и у вас mstshash действительно отражает нужный идентификатор, лимитируйте по
$rdp_cookie_mstshash
. Если нет — по$binary_remote_addr
. Комбинируйте черезmap
.Всегда включайте
zone
у upstream иstatus_zone
у listener«ов.Для RDP включайте
rdp_preread on;
именно там, где будет использоваться$rdp_cookie
. Это может быть как весь stream, так и конкретныйserver
. Следите заpreread_buffer_size
иpreread_timeout
.Выбирайте режим sticky осознанно.
learn
хорош как дефолт для RDP.route
— когда заранее известныsid
и вы хотите пружинящую, но контролируемую маршрутизацию. Не забудьте объявитьsticky
после метода балансировки.Не храните лишнее в логах. Маскируйте cookie. В access‑лог под stream это делается обычным
map
иlog_format
сescape=json
.Если у вас 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 месяцев и менять их по ходу. Гибкость плюс экономия — редкое сочетание.