Швидкісні концентратори VPN, сервери завершення сесій TLS та пограничні корпоративні маршрутизатори працюють під безперервним тиском масового відкриття нових з'єднань. За умов стійкого навантаження у десятки тисяч запитів на секунду пропускна здатність вузла визначається не лише криптографічною швидкістю чи смугою пропускання інтерфейсів. Стабільність інфраструктури часто опиняється під загрозою через навалу неактивних рукостискань TCP, роботу сканерів відкритих портів та атаки вичерпання пам'яті за зразком Slowloris.
У класичному мережевому стеку Linux завершення стандартного триетапного рукостискання (SYN -> SYN-ACK -> ACK) миттєво перетворює запит на повноцінний сокет. Ядро вміщує сокет у чергу очікування та інформує простір користувача через механізм epoll. Демон проксі прокидається, викликає accept(), виділяє структури збереження стану сесії та додає дескриптор до циклу подій — лише для того, щоб пасивно очікувати корисні дані, які можуть надійти із затримкою або не прийти взагалі.
Щоб позбутися зайвих перемикань контексту процесора та убезпечити простір користувача від шквалу порожніх з'єднань, у Linux реалізовано опцію сокета TCP_DEFER_ACCEPT. Вона вказує ядру затримати переведення з'єднання до черги accept доти, доки клієнт не надішле перший байт корисного навантаження. Завдяки цьому TCP_DEFER_ACCEPT переносить початковий фільтр у компактну пам'ять ядра, формуючи надійний рубіж оборони для високонавантажених VPN-шлюзів.
Криза порожніх рукостискань: Зайві пробудження Epoll та вичерпання стану
За стандартної роботи протоколу TCP встановлення з'єднання спирається на дворівневу структуру черг у мережевому стеку ядра Linux:
- Черга SYN Backlog: Отримавши запит TCP SYN, ядро створює невелику структуру
request_sock(розміром близько 256 байтів) і розміщує її в черзі SYN під час надсилання клієнту пакета SYN-ACK. - Черга Accept Queue: Щойно клієнт надсилає фінальний пакет ACK, ядро виділяє повноцінний об'єкт
struct sock(який займає понад 2 КБ зафіксованої оперативної пам'яті, включно з буферами надсилання та приймання), переносить з'єднання до черги готових сокетів і позначає дескриптор прапорцем готовності до читання (EPOLLIN).
Вузьке місце стандартної черги прийому з'єднань:
Клієнт Мережевий стек ядра Linux Проксі в просторі користувача
│ │ │
│─── SYN ──────────────────────────────────►│ │
│◄── SYN-ACK ───────────────────────────────│ (Створюється request_sock ~256 Б)│
│ │ │
│─── ACK (Порожнє рукостискання) ──────────►│ │
│ │──► Виділення struct sock │
│ │──► Розміщення в Accept Backlog │
│ │──► Сигнал EPOLLIN ──────────────►│
│ │ │ (Пробудження потоку)
│ │◄── accept4() ────────────────────│ (Виділення 4-16 КБ)
│ │ │ (Очікування даних...)
│ │ │
│... (Клієнт не надсилає нічого) ...........│..................................│ (Потік заблоковано!)
Така типова поведінка породжує серйозні слабкості в архітектурі пограничних вузлів:
- Даремні витрати процесорного часу: Робочі потоки регулярно активуються, виконують системні виклики та резервують ресурси пам'яті для з'єднань, через які не передається жодної корисної інформації.
- Роздування таблиці відкритих файлів: Автоматичні мережеві сканери успішно відкривають сесії та залишають їх у стані спокою, наближаючи лічильник дескрипторів до системного ліміту
RLIMIT_NOFILE. - Переповнення черги очікування: Під час сплесків трафіку порожні з'єднання переповнюють чергу прослуховування, через що запити реальних клієнтів відкидаються ще до початку їхньої обробки.
Глибокий аналіз: Як TCP_DEFER_ACCEPT змінює автомат станів TCP ядра
Опція TCP_DEFER_ACCEPT, яку налаштовують за допомогою виклику setsockopt(), докорінно змінює взаємодію між мережевим стеком ядра та подієвим циклом програми:
int timeout_secs = 30;
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
/* Наказ ядру відкласти сповіщення accept() до появи перших даних */
int ret = setsockopt(listen_fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout_secs, sizeof(timeout_secs));
1. Подовження стану SYN_RECV та відтермінування виділення повного сокета
Якщо на сокеті активовано параметр TCP_DEFER_ACCEPT, ядро отримує завершальний ACK, але навмисно зупиняє просування сокета за життєвим циклом. Якщо цей пакет ACK не містить корисної інформації (skb->len == 0), ядро утримує з'єднання в компактному стані SYN_RECV у таблиці request_sock.
Сокет не перетворюється на повний struct sock, а прапорець EPOLLIN не надсилається до програми. Для клієнта канал зв'язку виглядає цілком активним. Водночас для сервера він споживає мінімум пам'яті та не вимагає жодних обчислювальних витрат у просторі користувача.
2. Миттєва наявність даних під час системного виклику accept()
Щойно віддалений клієнт надсилає свій перший пакет із даними — наприклад, повідомлення TLS ClientHello, запит HTTP CONNECT або пакет запуску сесії WireGuard через TCP — ядро знаходить відповідну структуру request_sock.
Принцип роботи TCP_DEFER_ACCEPT:
Клієнт Мережевий стек ядра Linux Проксі в просторі користувача
│ │ │
│─── SYN ──────────────────────────────────►│ (Створено request_sock) │
│◄── SYN-ACK ───────────────────────────────│ │
│─── ACK (Без даних) ──────────────────────►│ (Лишається в SYN_RECV; НЕМАЄ IPI)│ (Потоки сплять / вільні)
│ │ │
│... (Пауза: 500 мс) .......................│ (Займає лише ~256 байтів пам'яті)│ (0% втрат процесора)
│ │ │
│─── PSH, ACK (Пакет TLS ClientHello) ─────►│──► Переведення у повний сокет │
│ │──► Додавання до черги Accept │
│ │──► Сигнал EPOLLIN ──────────────►│ (Пробудження потоку)
│ │◄── accept4() ────────────────────│
│ │◄── recv() (Дані готові одразу) │
Отримавши корисний пакет, ядро переводить з'єднання у стан TCP_ESTABLISHED, створює повноцінний сокет і спрямовує його до черги accept. Коли фоновий процес користувацького простору активується й виконує виклик accept(), корисне навантаження вже розміщене в буфері сокета. Наступний виклик recv() повертає байти миттєво без потреби повторного очікування.
Захист від атак: Нейтралізація Slowloris та фонового сканування портів
У відкритих публічних мережах TCP_DEFER_ACCEPT виступає як надійний системний щит проти атак вичерпання ресурсів рівня застосунків та небажаного мережевого моніторингу.
1. Поглинання навантаження у компактних структурах Request Sockets
Атаки Slowloris відкривають безліч TCP-з'єднань, не надсилаючи заголовків прикладного протоколу. Без параметра TCP_DEFER_ACCEPT зловмисник здатен вичерпати пул вільних дескрипторів шлюзу за мінімальної пропускної здатності каналу.
З активованим TCP_DEFER_ACCEPT такі сесії блокуються в таблиці request_sock ядра і не досягають користувацького простору. Демон проксі зберігає стабільність, спрямовуючи ресурси лише на обслуговування реальних клієнтів із корисним трафіком.
2. Автоматичне скидання за таймаутом і повторне надсилання SYN-ACK ядром
Числовий параметр, що передається у TCP_DEFER_ACCEPT, визначає максимальний час очікування в секундах. Якщо клієнт завершив рукостискання, але не передав дані до завершення цього строку, ядро повторно надсилає пакет SYN-ACK для стимулювання активності.
Якщо клієнт продовжує мовчати, ядро самостійно видаляє структуру request_sock. Неактивне з'єднання безшумно усувається з пам'яті без запису зайвих повідомлень про помилки та без додаткових операцій очищення у просторі користувача.
Практичний план: Налаштування TCP_DEFER_ACCEPT у робочих проксі-серверах
Застосування TCP_DEFER_ACCEPT на продуктивних вузлах VPN вимагає коректного визначення часу очікування та узгодження з неблокуючими подієвими циклами:
/* Створення захищеного слухаючого сокета */
int setup_hardened_listener(int port, int defer_timeout) {
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
int optval = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
/* Увімкнення відкладеного accept з таймаутом (наприклад, 15-30 секунд) */
setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &defer_timeout, sizeof(defer_timeout));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
listen(fd, 4096);
return fd;
}
Ключові експлуатаційні поради:
- Підбір таймауту: Для загальнодоступних вузлів рекомендовано встановлювати
defer_timeoutвід 15 до 30 секунд. Занадто короткі інтервали (до 5 секунд) можуть помилково скидати справжні підключення в мобільних мережах із відчутними затримками. - Вимоги до поведінки клієнтів: Переконайтеся, що протоколи клієнтських програм передають дані одразу після з'єднання. Стандарти TLS, HTTP, SSH та інкапсульований тунель WireGuard суворо дотримуються цієї логіки.
Архітектурна взаємодія: TCP Fast Open, SYN Cookies та погранична маршрутизація
TCP_DEFER_ACCEPT гармонійно поєднується з іншими захисними механізмами ядра Linux:
- TCP SYN Cookies: Під час масованих хвиль SYN-флуду ядро переходить до безстанової обробки через cookies. Коли клієнт повертає підтвердження,
TCP_DEFER_ACCEPTпродовжує стримувати створення сокета до надходження корисного навантаження, надійно захищаючи оперативну пам'ять. - Порівняння з TCP Fast Open (TFO): Якщо TFO надсилає дані безпосередньо у першому пакеті SYN (RFC 7413), то
TCP_DEFER_ACCEPTоптимізує стандартні сесії, де корисний вміст передається слідом. Поєднання обох підходів забезпечує високу ефективність для сучасних та старіших клієнтів.
Тракт даних RiceGuard: Фільтрація з'єднань з нульовими витратами на межі мережі
VPNRice застосовує механізм TCP_DEFER_ACCEPT як базовий компонент захисту вхідного трафіку на пограничних вузлах RiceGuard. Під час обслуговування корпоративних шлюзів та розподілених точок доступу стійкість до мережевого шуму та атак перевантаження є першочерговим завданням.
Завдяки активації відкладених рукостискань на всіх відкритих портах TCP вузли RiceGuard гарантують, що потоки обробки запускаються виключно для клієнтів із реальними запитами. Порожні підключення, фонові сканування та завислі сесії нейтралізуються та вичищаються на рівні ядра без навантаження на центральний процесор. Цей системний захист підтримує стабільну пропускну здатність, зберігає обчислювальні ресурси та гарантує конфіденційність кожної сесії користувача.



