Кое-что о TCP-стеке, атаках synflood, accept_filter и параметре listen backlog

Dec 02, 2012 00:59

При высоких нагрузках и DoS-атаках типа SYN flood бывает необходимо тюнить параметры системы. При этом надо помнить, что последовательность «рубежей защиты» обычно такова, с отсевом на каждой стадии, и глючить может каждый прежде всего надо смотреть их настройки:
  1. Входные балансировщики, например Cisco SLB
  2. Файрвол/IDS на самом хосте, например, pf (synproxy, лимит стейтов и др.)
  3. TCP/IP стек ядра FreeBSD
  4. Наконец, само приложение (например, nginx)
Ниже речь будет о последних двух, это, по большому счету, пересказ http://sysoev.ru/freebsd/netstat.html и фрагментов man-страниц listen(2) и accf_data(9) с мелкими добавками и приложением к современной ситуации. Следует понимать разницу между разными параметрами настройки (например SYN flood имеет лишь опосредованное отношение к длине очередей), поэтому и ведется сей рассказ.

У слушающего сокета в ядре есть 2 очереди: соединений с завершенным TCP 3-way handshake и незавершенных. Последовательность подключения нового клиента по TCP на сервере классически выглядит так:
  1. Клиент прислал SYN. Ядро сервера создает новый сокет и TCP Control Block на новое соединение (сравнительно затратно, можно посмотреть в vmstat -z сумму socket+tcp_inpcb+tcpcb).
  2. Соединение в состоянии SYN_RCVD помещается в incomplete-очередь сокета. Ядро отсылает SYN+ACK, с таймерами перепосылки.
  3. Клиент прислал ACK. Соединение переходит в состояние ESTABLISHED и помещается в очередь готовых соединений
Далее должны произойти, в заранее неизвестной последовательности:
  • Сервер должен сделать на сокете accept(), чем вынет готовое соединение из очереди. После этого ядро выделит еще память на файловый дескриптор - сравнительно немного, но сервер затратит на нового клиента уже свои ресурсы в юзерленде, обычно куда более значительные (вплоть до форка целого процесса).
  • Клиент пришлет данные запроса (протоколы, где сначала должен ответить сервер, рассматривать не будем), которые сервер может читать и обрабатывать.
Атаки типа SYN flood направлены, как известно, на исчерпание ресурсов ядра - на шаге 1 они сразу выделяются на полноценное соединение. Во FreeBSD 4.5 появился механизм syncache(4), который выделяет лишь минимальные данные вместо полноценного соединения (хранение опций пакета и таймер перепосылки для шага 2) - порядка 100-150 байт, точное значение можно посмотреть в vmstat -z | grep syncache. Теперь соединение (новый сокет и TCP Control Block) создается только на шаге 3 (технически сначала в SYN_RCVD, как по-старому, но лишь на микросекунды). Размер кэша регулируется в loader.conf, на 9.0 значение по умолчанию:

# sysctl net.inet.tcp.syncache.cachelimit
net.inet.tcp.syncache.cachelimit: 15360

При нехватке и этого кэша можно использовать механизм syncookie (описан в том же мане), он вообще не использует память на сервере - данные хранятся в опциях SYN-пакета, естественно, ценой потери возможности window scaling, MSS и др. (существует и потенциальная вероятность атак на файрволы). Поэтому их обычно предпочитают не использовать.

Время существования записи в syncache зависит от числа перепосылок, настраивается в sysctl net.inet.tcp.syncache.rexmtlimit. Для значения по умолчанию исходники сообщают, что 3 retransmits corresponds to a timeout of 3 * (1 + 2 + 4 + 8) == 45 seconds. Надо заметить, что здесь не следует путать значение по умолачнию 3 в sysctl с множителем 3 в формуле - это константа в секундах, то есть, если задать 4 в sysctl, то станет уже 3 * (1 + 2 + 4 + 8 + 16) == 93 секунды, и т.д. (подробнее см. описания работы TCP, например, у Стивенса).

Поскольку при этих механизмах соединение создается сразу готовым, incomplete-очередь более не используется. Текущий размер очередей можно посмотреть так:

# netstat -Lan
Current listen queue sizes (qlen/incqlen/maxqlen)
Listen Local Address
2/0/3000 *.80
0/0/5 *.22
Здесь qlen - очередь готовых соединений, а incqlen будет всегда равен нулю. Но, на самом деле, не всегда: с FreeBSD 4.1 появился механизм accept-фильтров - модулей ядра, которые задерживают передачу соединения приложению в accept() до тех пор, пока клиент не пришлет данные запроса, чтобы сэкономить на числе вызовов (переключении контекста) при большой нагрузке. Такие соединения, хотя и находятся в ESTABLISHED, отображаются теперь под incqlen, считаясь в этой очереди. Для того, чтобы accept-фильтры использовались, они должны быть явно запрошены в коде самим приложением (а модуль ядра должен быть загружен до этого).

Остается параметр maxqlen. Он всегда равен параметру backlog, передаваемому в вызов listen(2) (под именем backlog информацию о нем и можно найти в разных источниках). Этот параметр исторически имел размытое определение и трактовался по-разному на разных ОС. На *BSD его значение определяло сумму длин разных очередей, а потому умножалось на полтора, чтобы «с запасом». Сейчас, ввиду всего вышесказанного, он работает несколько иначе, суммы уже нет, есть два отдельных параметра:
  • длина очереди incomplete-соединений (incqlen) в точности ограничена значением backlog
  • длина очереди готовых соединений может быть больше backlog в полтора раза
Умножение производится только при создании нового соединения; собственно же умноженное значение нигде не хранится и не показывается, в netstat отображается всегда то, что передало приложение. Код syncache вызывает на шаге 3 sonewconn(lso, SS_ISCONNECTED);, где код выглядит примерно так:

struct socket *
sonewconn(struct socket *head, int connstatus)
{
over = (head->so_qlen > 3 * head->so_qlimit / 2);
if (over)
return (NULL);
if (на сокете включен SO_ACCEPTFILTER)
connstatus = 0;
...
if (connstatus)
добавить в complete-очередь, qlen++
else
добавить в incomplete-очередь (убив при необходимости самое старое), incqlen++
Таким образом, backlog * 1.5 выступает в качестве жесткого лимита для обеих очередей, приблизительно сохраняя историческую семантику.

Что происходит, если очереди переполняются? Соединение просто сбрасывается, возвращается TCP RST (Connection refused).

На что все эти очереди влияют? Значения в них, в принципе, могут быть довольно малы, поскольку в норме соединения не должны в них задерживаться. Это всего лишь временная очередь, пока приложение не сделало accept(), например, будучи занято обработкой чего-то еще, при большой нагрузке. Таким образом, при отсутствии ошибок в приложении, очереди будут нужны лишь при кратковременных всплесках, отражая в это время скорее скорость прибытия новых пользователей, чем общее количество.

Приложения, проблемы

Обычно настройку длины очереди можно найти по упоминавшемуся имени, либо она по умолчанию имеет достаточное значение, не требующее настройки. В том же nginx директиве listen можно задать так и называющиеся параметры - backlog=32000 accept_filter=httpready. Но, например, BIND отличился - в нем иначе и то, и другое. BIND 9 Administrator Reference Manual (Bv9ARM.pdf) именует параметр backlog иначе:

tcp-listen-queue

The listen queue depth. The default and minimum is 3. If the kernel supports the accept filter ”dataready” this also controls how many TCP connections that will be queued in kernel space waiting for some data before being passed to accept. Values less than 3 will be silently raised.

То есть, в принципе до запуска BIND можно делать:

kldload accf_data
Еще следует отметить, что раньше были только accf_data и accf_http. В последнее время появился еще accf_dns, но пока BIND его не поддерживает. Стоит вернуться к этой теме в будущем.

Разработчики nginx считают (пост 1, пост 2, что реализация accept_filter'ов (и TCP_DEFER_ACCEPT, аналога accf_data в Linux) в их нынешнем виде - скорее зло, чем благо. Почему? Потому что соединение может висеть в этом состоянии сколь угодно долго, и будет вытеснено только при достижении хвоста очереди. Такие приложения, как nginx, могли бы контролировать поступление данных по своим таймаутам.

Теоретически, механизм accept_filter'ов во FreeBSD позволяет это сделать вполне безболезненно - в вызове setsockopt(2) имеется аж 240 байт на передачу аргумента в фильтр. Так что желающие вполне могут реализовать, patches are welcome, как говорится.

И, в тему к обсуждению проблем - тот же accf_http имеет несколько ограничений в своей работе, из-за чего он в некоторых случаях как бы не работает. Точнее, любой accept_filter при возникновении любых непонятных ситуаций ложится спать ведет себя так, как будто его нет: тут же передает соединение в приложение - приложению лучше знать, что делать. Так вот, accf_http поступает так при отличающейся от 1.0 или 1.1 версии HTTP (если включен соответствующий sysctl), если буфер сокета заполнился - а также, если HTTP-метод был им не распознан. А понимает он их только два - GET и HEAD. Причем именно так, большими буквами, с начала строки. Если что-то не так - всё, коннект сразу идет в приложение. В общем, тоже имеется простор для улучшений...

Ссылки: об управлении памятью в ядре FreeBSD (упоминавшемся vmstat) и других затратах на буфера для соединения можно прочитать тут.

сети, объяснение, freebsd

Previous post Next post
Up