Почти год назад мы реализовали фичу строго по RFC — и положили продакшен. Откатили, разобрались, нашли причину. Спойлер: дело было не в DNS. Мы просто забыли, что порты — ресурс конечный.
TL;DR
Мы добавили полноценную поддержку SOCKS5 UDP ASSOCIATE (RFC 1928) на прокси-серверах iProxy.online. Через пару дней DNS перестал резолвить, туннели начали падать, а случайные сервисы уходили в таймаут. Откатили и начали копать. Корневая причина — исчерпание эфемерных портов на Linux: тысячи UDP-ассоциаций держали по выделенному порту, полностью осушив системный пул. Починка свелась к двум параметрам конфигурации. Найти проблему заняло куда больше времени.
Что такое SOCKS5 UDP ASSOCIATE?
SOCKS5, описанный в RFC 1928 , поддерживает три команды: CONNECT (TCP-туннелирование), BIND (приём входящих TCP-соединений) и UDP ASSOCIATE (ретрансляция UDP-датаграмм). Большинство провайдеров прокси реализуют только CONNECT. UDP ASSOCIATE — самая сложная команда и единственная, которая нужна для real-time приложений: VoIP, игры, DNS-over-UDP, QUIC .
Как это работает:
- Клиент открывает TCP control connection к SOCKS5-серверу.
- По этому TCP-соединению отправляет запрос UDP ASSOCIATE.
- Сервер выделяет отдельный UDP-сокет на эфемерном порту и возвращает клиенту адрес и номер порта.
- Клиент шлёт UDP-датаграммы на этот relay-порт. Сервер пересылает их на целевой адрес и ретранслирует ответы обратно.
- Ассоциация живёт, пока не закроется TCP control connection или не истечёт таймаут сессии.
Каждая UDP-ассоциация занимает отдельный эфемерный порт на сервере. Это ключевая деталь.
И вот ловушка. UDP — протокол без соединения. Нет FIN, нет RST. Как узнать, что клиент закончил работу, и освободить порт?
- Ждать закрытия TCP control connection — сигнал по RFC. Но клиент может держать его открытым бесконечно.
- Idle timeout. Если за X секунд через relay-порт не прошло ни одной датаграммы — убиваем ассоциацию. Но если таймаут слишком щедрый, сокеты копятся.
Мы выставили щедрые таймауты и не ограничили количество ассоциаций на клиента. Итог: сокеты копились.
Наша инфраструктура
В iProxy.online мы управляем инфраструктурой мобильных прокси в 100+ странах и через 600+ мобильных операторов. Реальные Android-телефоны превращаются в прокси-серверы. Если ты только разбираешься в теме, в нашем гайде по созданию 4G прокси-сети подробно описана архитектура. Наши бэкенд SOCKS5-серверы обрабатывают десятки тысяч одновременных соединений.
Около года назад мы решили реализовать полноценный UDP ASSOCIATE — стать по-настоящему RFC-совместимыми и открыть юзкейсы, которые конкуренты не поддерживают: проксирование VoIP, игрового трафика, QUIC-протоколов. Реализация была корректной. Тесты прошли. Выкатили в прод.
Прокси-инфраструктура такого масштаба — это когда каждое решение по протоколу имеет реальные последствия. В iProxy.online каждое Android-устройство работает как независимый мобильный прокси-сервер — SOCKS5, HTTP и теперь полный UDP — с управлением через единый дашборд или Telegram-бот . Хочешь посмотреть, как всё устроено, прежде чем читать, как мы это сломали? Создай мобильный прокси бесплатно на 48 часов — без привязки карты.
Что пошло не так
Через пару дней на продакшен-серверах начали проявляться странные, казалось бы, не связанные друг с другом симптомы:
- DNS перестал резолвить. Внутренние сервисы не могли найти друг друга по хостнейму.
- Туннели падали. Управляющая связь с серверами терялась.
- NTP-синхронизация отвалилась. Часы серверов начали расходиться.
- Случайные UDP-сервисы уходили в таймаут без видимой закономерности.
Отказ был не плавным, а обрывным. Всё работало нормально часами, а потом несколько сервисов падали одновременно на одном хосте. Мы откатили UDP ASSOCIATE и начали разбирать.
Поиск корневой причины
Первый инстинкт оказался неверным. Проверили DDoS, утечки памяти, нагрузку на диск, CPU — всё в норме. Application-level health checks были зелёными ровно до момента, когда всё умирало.
Прорыв дали метрики уровня ядра, которые большинство команд даже не мониторит:
node_sockstat_UDP_inuse рос в десятки тысяч. На здоровом сервере этот показатель — пара сотен. У нас перевалило за 20K.
Счётчики ICMP Type 3 Code 3 (port unreachable) резко выросли. Это ядро говорит: «Не могу выделить исходный порт для исходящего UDP-пакета».
Ручная проверка подтвердила:
ss -u state all | wc -l
# 28 431
cat /proc/net/sockstat
# UDP: inuse 28419
Диапазон эфемерных портов Linux по умолчанию — 32768–60999, примерно 28 000 портов. Мы использовали почти все.
Каскадный отказ
Арифметика простая. У Linux конечный пул эфемерных портов — около 28K по умолчанию. Каждый UDP ASSOCIATE съедает один. Сотни одновременных ассоциаций плюс медленная очистка — пул исчерпан.
Когда эфемерные порты кончаются, всё на сервере, что пытается открыть новый UDP-сокет, падает. Включая DNS — каждый исходящий DNS-запрос требует эфемерного порта , чтобы отправить пакет на порт 53. Резолвинг имён умирает, и вдруг всё на сервере сломано по причинам, которые к DNS не имеют никакого отношения.
Каскад:
Выделение портов → каждый UDP ASSOCIATE вызывает bind() с портом 0, запрашивая у ядра следующий доступный эфемерный порт.
Накопление портов → порт остаётся занятым до закрытия TCP-соединения или срабатывания idle timeout. Щедрые таймауты — порты копятся быстрее, чем освобождаются.
Исчерпание пула → тысячи ассоциаций держат по порту, весь пул вычерпан. bind() начинает возвращать EADDRINUSE на каждый новый сокет.
Системный отказ → DNS-запросы падают (systemd-resolved тоже нужны эфемерные порты). WireGuard-хендшейки не проходят. NTP не работает. Syslog-over-UDP умирает тихо. Затем отказ DNS вызывает вторичный каскад — всё, что резолвит хостнеймы, перестаёт работать: health checks, подключения к базам, агенты мониторинга. Сервер выглядит «лежащим», хотя CPU, память и диск в норме.
Почему стандартный мониторинг не поймал
HTTP health checks проходили — эндпоинт слушал. CPU, память, диск, пропускная способность — всё штатно. Метрики на уровне процессов — ничего необычного. SOCKS5-процесс был здоров. Исчерпался пул портов ядра, а ни один стандартный дашборд Grafana это не отслеживает.
Единственные метрики, которые сработали, мы добавили почти «на всякий случай»: счётчики сокетов на уровне ядра и частота ICMP-ошибок.
Решение
Починили двумя настройками. Дебаг занял куда больше времени, чем сам фикс.
1. Резко сократили idle timeout. Снизили таймаут бездействия UDP-ассоциаций с минут до секунд. Если через relay-порт за короткий интервал не прошло ни одной датаграммы — ассоциация рвётся, порт освобождается. Большинство легитимных UDP-сессий (DNS-запросы, NTP) завершаются за доли секунды. Долгоживущие сессии (VoIP, игры) поддерживают ассоциацию регулярным трафиком и не страдают.
2. Лимиты на одновременные ассоциации на клиента. Ограничили, сколько UDP-ассоциаций один клиент может держать одновременно. Это не даёт одному пользователю — или кривому клиенту — монополизировать пул портов. Лимит достаточно щедрый для реального использования, но останавливает неконтролируемое накопление.
Вместе эти меры вернули UDP_inuse с 28K до нескольких сотен. Перевыкатили UDP ASSOCIATE с новыми лимитами — с тех пор стабильно.
Сам фикс был простым — сложнее было построить поддержку SOCKS5 UDP ASSOCIATE, которая выдерживает десятки тысяч одновременных сессий и не жрёт системные ресурсы. Нужен мобильный прокси с полноценным UDP relay и правильным управлением жизненным циклом? iProxy.online работает на реальных Android-устройствах через 600+ операторов , с ротацией IP , поштучным управлением устройствами и тарифами от $6/мес. Начать бесплатный 48-часовой триал →
Что нужно мониторить
Если ты эксплуатируешь что-то, что массово открывает UDP-сокеты — SOCKS5 прокси, игровые серверы, VoIP-инфраструктуру, QUIC-балансировщики — добавь в стек мониторинга:
node_sockstat_UDP_inuse(node_exporter). Открытые UDP-сокеты в реальном времени. Норма — пара сотен. Если используешь Prometheus, метрика уже есть — нужна только панель и алерт. Рекомендуем порог — 5 000.node_netstat_Icmp_OutDestUnreachs(ICMP Type 3 Code 3, port unreachable). Всплески означают, что ядро отвечает на UDP-пакеты, прилетающие на порты, которые никто не слушает. Единицы в минуту — шум. Тысячи в секунду — пожар.ss -u state all | wc -l— быстрая проверка руками во время инцидента.cat /proc/net/sockstat— классический однострочник без зависимостей.
Эти метрики не входят в стандартные дашборды. А должны. Подробнее о том, как поддерживать прокси-инфраструктуру в здоровом состоянии — в нашем гайде по оптимизации скорости и стабильности прокси .
Выводы
Порты — конечный ресурс, планируй их расход. Мы бюджетируем CPU, RAM, диск, полосу. Эфемерные порты не бюджетирует никто. На сервере с десятками тысяч соединений ~28K портов — это немного. Расширить диапазон можно через sysctl -w net.ipv4.ip_local_port_range="1024 65535", но даже 64K конечны, если каждая ассоциация держит порт неопределённо долго.
RFC говорит ЧТО, а не КАК. RFC 1928 написан в 1996 году, когда «нагруженный» сервер обрабатывал сотни соединений. Механика протокола описана идеально. Про управление жизненным циклом портов, лимиты ресурсов или graceful degradation — ни слова. Если реализуешь любой протокол на масштабе — читай RFC для корректности, а управление ресурсами проектируй сам.
Метрики ядра ловят то, что пропускают метрики приложений. Health checks, Prometheus-скрейперы, HTTP-пинги — все говорили, что серверы здоровы. Ядро знало лучше. Если в мониторинге нет статистики сокетов и ICMP-счётчиков — у тебя слепая зона для целого класса отказов из-за исчерпания ресурсов. Мы столкнулись с похожей проблемой наблюдаемости при обнаружении скрытых сбоев TLS 1.3 на нашем флоте Android-устройств — другая причина, тот же урок.
UDP требует явного управления жизненным циклом. У TCP есть встроенный lifecycle — соединения открываются, передают данные и закрываются через определённый хендшейк. Порты переиспользуются после TIME_WAIT. У UDP ничего этого нет. Сокет висит открытым, пока что-то явно его не закроет. В relay-архитектурах нужно строить свой lifecycle, иначе потребление ресурсов растёт неограниченно.
Кому ещё стоит об этом задуматься
Это не только проблема прокси. Любая инфраструктура, выделяющая UDP-сокеты по запросам пользователей, может упереться в тот же обрыв:
- TURN/STUN-серверы для WebRTC — каждый медиа-relay выделяет пару портов.
- Игровые серверы — сессия каждого игрока может занимать UDP-порт.
- QUIC-балансировщики — миграция соединений ведёт к накоплению портов.
- Рекурсивные DNS-резолверы — каждый исходящий запрос использует эфемерный порт.
- VPN-концентраторы — WireGuard и IPsec IKE работают поверх UDP.
Если ты эксплуатируешь что-то из этого на масштабе — или управляешь фермой мобильных прокси
— проверь sockstat сегодня. Возможно, ты ближе к обрыву, чем думаешь.
Итог
Мы реализовали SOCKS5 UDP ASSOCIATE корректно — по RFC, с тестами, задеплоили. И получили системный отказ, невидимый для стандартного мониторинга.
Вывод, который мы теперь считаем железным правилом: любая фича, выделяющая ресурсы на уровне ядра — порты, файловые дескрипторы, записи conntrack — требует явного управления жизненным циклом и бюджетирования ресурсов с первого дня. Не как фикс после первого аутейджа.
Порты как кислород. Не замечаешь, пока не кончатся.
Это реальный production-инцидент iProxy.online. Мы строим инфраструктуру мобильных прокси, превращая Android-телефоны в SOCKS5 и HTTP прокси-серверы в 100+ странах через 600+ операторов. Полная поддержка UDP ASSOCIATE — теперь с правильным управлением ресурсами. Попробовать iProxy →