Майже рік тому ми реалізували 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 — найскладніша команда, і водночас та, що потрібна для реальних застосувань: VoIP, ігровий трафік, DNS-over-UDP, QUIC .
Як це працює:
-
Клієнт відкриває TCP-з’єднання керування з SOCKS5-сервером.
-
Через це TCP-з’єднання клієнт надсилає запит UDP ASSOCIATE.
-
Сервер виділяє окремий UDP-сокет на ефемерному порту та повертає адресу й номер порту.
-
Клієнт відправляє UDP-датаграми на цей relay-порт. Сервер пересилає їх до адресата та повертає відповіді.
-
Асоціація живе, поки TCP-з’єднання керування не закриється або не спрацює таймаут сесії.
Кожна UDP-асоціація займає окремий ефемерний порт на сервері. Ось ключова деталь.
А ось пастка. UDP — протокол без з’єднання. Ніякого FIN, ніякого RST. Як зрозуміти, що клієнт закінчив, і звільнити порт?
-
Чекати закриття TCP-з’єднання керування — сигнал за RFC. Але клієнти можуть тримати його відкритим нескінченно.
-
Idle timeout. Якщо датаграми не приходять X секунд — вбиваємо асоціацію. Але якщо таймаут завеликий, сокети накопичуються.
Ми виставили щедрі таймаути й не обмежили кількість асоціацій на одного клієнта. Сокети накопичились.
Наша інфраструктура
У iProxy.online ми керуємо інфраструктурою мобільних проксі у 100+ країнах і 600+ мобільних операторах, перетворюючи реальні Android-пристрої на проксі-сервери. Якщо ви новачок у цій темі, наш гайд про побудову мережі 4G-проксі детально описує архітектуру. Наші бекенд-сервери SOCKS5 обробляють десятки тисяч одночасних з’єднань.
Приблизно рік тому ми вирішили впровадити повну підтримку UDP ASSOCIATE — стати по-справжньому RFC-сумісними та відкрити юзкейси, які більшість конкурентів не тягне: проксіювання VoIP, ігрового трафіку, QUIC-протоколів. Реалізація була коректною. Тести пройшли. Викатили.
Проксі-інфраструктура такого масштабу — це коли кожне рішення щодо протоколу має реальні наслідки. У iProxy.online кожен Android-пристрій працює як окремий мобільний проксі сервер — SOCKS5, HTTP і тепер повний UDP — з керуванням через єдиний дашборд або Telegram-бот . Хочете побачити, як система працює, перш ніж читати, як ми її зламали? Створити мобільний проксі — безкоштовний 48-годинний тріал, без прив’язки картки.
Що пішло не так
За кілька днів після деплою на продакшн-серверах почалися дивні, на перший погляд не пов’язані між собою симптоми:
- DNS перестав резолвити. Внутрішні сервіси не могли знайти один одного за hostname.
- Тунелі відвалювались. Управлінське з’єднання з серверами втрачалось.
- Синхронізація NTP зламалась. Годинники серверів почали «плисти».
- Випадкові UDP-сервіси таймаутили без зрозумілої закономірності.
Збій був не поступовим — а обривним. Все працювало нормально годинами, а потім кілька сервісів падали одночасно на одному хості. Ми відкотили UDP ASSOCIATE і почали розбиратися.
Пошук кореневої причини
Перший інстинкт виявився хибним. Перевірили DDoS-атаки, витоки пам’яті, навантаження на диск, CPU — все в нормі. Аплікаційні хелсчеки горіли зеленим аж до моменту, коли все вмирало.
Прорив прийшов від низькорівневих системних метрик, на які більшість команд навіть не дивиться:
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-асоціація з’їдає один. Сотні одночасних асоціацій плюс повільне очищення — і пул вичерпано.
Коли ефемерні порти закінчуються, будь-що на сервері, що намагається відкрити новий UDP-сокет, — ламається. Включно з DNS: кожен вихідний DNS-запит потребує ефемерного порту , щоб відправити запит на порт 53. Резолвінг імен помирає, і раптом все на сервері зламано з причин, які не мають жодного стосунку до DNS.
Каскад:
Виділення портів → кожна UDP-асоціація викликає bind() з портом 0, просячи ядро віддати наступний вільний ефемерний порт.
Накопичення портів → порт залишається відкритим, поки TCP не закриється або не спрацює idle timeout. Щедрі таймаути означають, що порти накопичуються швидше, ніж звільняються.
Вичерпання пулу → тисячі асоціацій тримають по порту, весь пул висихає. bind() починає повертати EADDRINUSE для кожного нового сокета.
Системний збій → DNS-запити не проходять (systemd-resolved теж потребує ефемерних портів). WireGuard-хендшейки ламаються. NTP ламається. Syslog-over-UDP мовчки помирає. Потім збій DNS спричиняє вторинний каскад — все, що резолвить hostname, перестає працювати, включно з хелсчеками, підключеннями до БД і моніторинговими агентами. Сервер виглядає «мертвим», хоча CPU, пам’ять і диск — в нормі.
Чому стандартний моніторинг не помітив
HTTP-хелсчеки проходили — ендпоінт слухав. 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 для коректності, а управління ресурсами проектуйте самі.
Метрики ядра бачать те, що аплікаційні — ні. Хелсчеки, Prometheus-скрейпери, HTTP-пінги — все казало, що сервери здорові. Ядро знало краще. Якщо ваш моніторинг не включає статистику сокетів і лічильники ICMP, у вас є сліпа зона для цілого класу збоїв через вичерпання ресурсів. Ми зіткнулися з аналогічним розривом у спостережуваності при виявленні прихованих збоїв TLS 1.3 на нашому парку Android-пристроїв — інша корінна причина, але урок той самий.
UDP потребує явного управління життєвим циклом. У TCP є вбудований lifecycle — з’єднання відкриваються, передають дані й закриваються визначеним хендшейком. Порти перевикористовуються після TIME_WAIT. У UDP цього немає. Сокет висить відкритим, поки щось явно його не закриє. У relay-архітектурах ви мусите будувати свій lifecycle, інакше — необмежене споживання ресурсів.
Кому ще варто хвилюватися
Це не тільки проксі-проблема. Будь-яка інфраструктура, що виділяє UDP-сокети за запитами користувачів, може впертися в той самий обрив:
- TURN/STUN-сервери для WebRTC — кожен медіа-релей виділяє пару портів.
- Ігрові сервери — сесія кожного гравця може тримати UDP-порт.
- QUIC-балансувальники — міграція з’єднань може призводити до накопичення портів.
- Рекурсивні DNS-резолвери — кожен вихідний запит використовує ефемерний порт.
- VPN-концентратори — WireGuard та IPsec IKE обидва працюють через UDP.
Якщо ви запускаєте щось із цього на масштабі — або керуєте фермою мобільних проксі
— перевірте свій sockstat сьогодні. Можливо, ви ближче до обриву, ніж думаєте.
Підсумок
Ми реалізували SOCKS5 UDP ASSOCIATE коректно — відповідно до RFC, протестували, задеплоїли. І він спричинив системний збій, якого наш стандартний моніторинг не побачив.
Висновок, який ми тепер вважаємо жорстким правилом: будь-яка фіча, що виділяє ресурси на рівні ядра — порти, файлові дескриптори, conntrack-записи — потребує явного управління життєвим циклом і бюджетування ресурсів з першого дня. Не як фікс після першого падіння.
Порти — як кисень. Не помічаєш, поки не закінчаться.
Це реальний продакшн-інцидент від iProxy.online. Ми будуємо інфраструктуру мобільних проксі, що перетворює Android-телефони на SOCKS5 та HTTP проксі-сервери, працюючи у 100+ країнах і 600+ операторів. Повна підтримка UDP ASSOCIATE включена — тепер із правильним управлінням ресурсами. Спробувати iProxy →