Высоконагруженные шлюзы VPN, серверы терминации TLS и пограничные корпоративные маршрутизаторы работают в условиях постоянного потока новых соединений. При непрерывной нагрузке в десятки тысяч запросов в секунду производительность узла определяется не только скоростью шифрования или пропускной способностью интерфейсов. Стабильность системы часто оказывается под угрозой из-за обилия праздных рукопожатий TCP, фонового сканирования портов ботами и атак на исчерпание соединений в стиле Slowloris.

В стандартном сетевом стеке Linux успешное завершение трехстороннего рукопожатия (SYN -> SYN-ACK -> ACK) мгновенно преобразует временный запрос в полноценный сокет. Ядро помещает сокет в очередь прослушивания и сигнализирует пространству пользователя через механизм epoll. Демон прокси просыпается, вызывает accept(), выделяет структуры отслеживания сессии и регистрирует дескриптор в цикле событий — лишь для того, чтобы перейти в режим ожидания клиентских данных, которые могут задержаться или не прийти вовсе.

Чтобы исключить лишние переключения контекста процессора и изолировать приложение от шквала пустых соединений, в Linux предусмотрена опция сокета TCP_DEFER_ACCEPT. Позволяя отложить перенос соединения в очередь accept до момента получения первого байта полезной нагрузки от клиента, TCP_DEFER_ACCEPT переносит первичную фильтрацию в легковесную память ядра, создавая надежный эшелон защиты для плотных VPN-шлюзов.


Проблема пустых рукопожатий: Лишние пробуждения Epoll и исчерпание ресурсов

В классическом поведении протокола TCP установка связи опирается на двухуровневую систему очередей в стеке ядра Linux:

  1. Очередь SYN Backlog: При получении пакета TCP SYN ядро создает компактную структуру request_sock (размером около 256 байт) и удерживает ее в очереди SYN на время отправки SYN-ACK.
  2. Очередь Accept Queue: Когда клиент возвращает финальный пакет ACK, ядро выделяет полноценную структуру struct sock (занимающую более 2 КБ закрепленной памяти ядра плюс буферы приема и передачи), переводит соединение в очередь готовых сокетов и помечает слушающий дескриптор флагом готовности к чтению (EPOLLIN).
Узкое место стандартной очереди приема соединений:

Клиент                              Сетевой стек ядра Linux             Прокси в user space
  │                                           │                                  │
  │─── SYN ──────────────────────────────────►│                                  │
  │◄── SYN-ACK ───────────────────────────────│ (Создается request_sock ~256 Б)  │
  │                                           │                                  │
  │─── ACK (Пустое завершение рукопожатия) ──►│                                  │
  │                                           │──► Выделение struct sock         │
  │                                           │──► Помещение в Accept Backlog    │
  │                                           │──► Сигнал EPOLLIN ──────────────►│
  │                                           │                                  │ (Пробуждение потока)
  │                                           │◄── accept4() ────────────────────│ (Выделение 4-16 КБ)
  │                                           │                                  │ (Пассивное ожидание)
  │                                           │                                  │
  │... (Клиент бездействует или сканирует) ...│..................................│ (Поток заблокирован!)

Такой алгоритм по умолчанию создает существенные уязвимости в архитектуре пограничного узла:

  • Бесполезный расход тактов CPU: Рабочие потоки непрерывно просыпаются, выполняют системные вызовы и резервируют память под соединения, не несущие никакой прикладной информации.
  • Раздувание таблицы дескрипторов: Автоматизированные сетевые сканеры завершают рукопожатие и оставляют соединения висеть, приближая число открытых файлов к лимиту RLIMIT_NOFILE.
  • Переполнение очереди прослушивания: При всплесках трафика пустые сессии забивают очередь ожидания, из-за чего запросы легитимных пользователей отбрасываются еще до того, как демон успеет их обработать.

Глубокий анализ: Как TCP_DEFER_ACCEPT меняет конечный автомат TCP в ядре

Включаемая с помощью вызова setsockopt(), опция TCP_DEFER_ACCEPT принципиально перестраивает логику взаимодействия сетевого стека с пользовательским циклом событий:

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() в user space

В тот момент, когда удаленная сторона отправляет свой первый пакет с полезной нагрузкой — например, TLS ClientHello, команду HTTP CONNECT или пакет инициализации WireGuard через TCP, — ядро находит соответствующую запись request_sock.

Схема работы TCP_DEFER_ACCEPT:

Клиент                              Сетевой стек ядра Linux             Прокси в user space
  │                                           │                                  │
  │─── SYN ──────────────────────────────────►│ (Создан request_sock)            │
  │◄── SYN-ACK ───────────────────────────────│                                  │
  │─── ACK (Без данных) ─────────────────────►│ (Остается в SYN_RECV; НЕТ сигнала│ (Потоки спят / свободны)
  │                                           │                                  │
  │... (Пауза: 500 мс) .......................│ (Занимает всего ~256 байт памяти)│ (0% нагрузки на CPU)
  │                                           │                                  │
  │─── PSH, ACK (Пакет TLS ClientHello) ─────►│──► Преобразование в полный сокет │
  │                                           │──► Добавление в очередь Accept   │
  │                                           │──► Сигнал EPOLLIN ──────────────►│ (Пробуждение потока)
  │                                           │◄── accept4() ────────────────────│
  │                                           │◄── recv() (Данные готовы сразу)  │

При поступлении полезной нагрузки ядро мгновенно переводит соединение в состояние TCP_ESTABLISHED, создает полноценный сокет и помещает его в очередь приема. Когда процесс пользовательского пространства активируется и вызывает accept(), данные уже находятся в приемном буфере сокета. Последующий вызов recv() возвращает байты без ожидания и без повторной постановки в epoll.


Защита от атак: Нейтрализация 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 гарантируют, что вычислительные потоки приложения запускаются только для клиентов с реальными запросами. Пустые подключения, попытки сканирования и незавершенные сессии изолируются и удаляются в ядре без нагрузки на процессор. Такой эшелонированный подход обеспечивает высокую пропускную способность, снижает накладные расходы и гарантирует надежную защиту конфиденциальности каждого пользовательского туннеля.