Как DHT в торрентах кладёт центральный роутер

10.06.2013 10:03

И КАК ЭТО ПОБОРОТЬ

Имеется средняя сетка на несколько тысяч абонентов, которая обслуживается центральным сервером на FreeBSD с сервисом NAT-трансляций. И есть лёгкий способ это всё дело категорически поставить на колени.

Суть проблемы

Проблема происходит так: сеть работает стабильно, без предпосылок к аварии. Внезапно процессор на NAT-сервере резко улетает в полку под 100%, отказывает в обслуживании и не пускает в свою консоль. Если отключаем исходящий трафик (внутреннюю сеть) — проблема не решается, если отключаем входящий трафик (аплинк), то процессор моментально расслабляется. Через 30-40 минут вся симптоматика проходит и не воспроизводится.

То, что сервер не отвечает в момент проблемы, существенно затрудняет диагностирование. Коммутатор на аплинке не показывает никакого аномального роста трафика: PPS и скорость потока в норме.

Поиск причин

Расскажу, как удалось локализовать проблему: наверняка многим будет полезно познакомиться с моим инструментарием на случай аналогичной проблемы.

Итак, в какой-то момент нам повезло и проблема продлилась больше, чем 30 минут. Я отключил от сети всех абонентов и стал выводить районами. Довольно быстро локализовал абонента, который провоцировал вредный трафик. Действительно, сервер прекрасно справлялся с тремя тысячами абонентов, но один единственный клиент укладывал его на лопатки.

Конфигурация NAT у нас самая обычная:

ipfw nat $NUM_NAT config igb0 ip $WHITE_IP
ipfw add nat tablearg ip from any to table(2) in recv vlan158
ipfw add nat tablearg ip from table(1) to any out xmit vlan158
ipfw table 1 add 10.1.1.1/32 1001
ipfw table 2 add 31.1.1.1/32 1001 

Таблица 1 содержит соответствие внутренних адресов и номеров NAT instance, таблица 2 содержит соответствие внешних адресов и номеров NAT instance.

Соответственно, чтобы найти источник вредного трафика, я очистил таблицу 2, а затем стал постепенно возвращать в нее записи, наблюдая в соседнем окне за показаниями TOP -SHP. Когда вредный абонент получает интернет, пару минут всё работает нормально, затем нагрузка начинает стремительно расти примерно по проценту в секунду. В этот момент я отключил абонента от сети, вернул всем клиентам сервис и приготовился к экспериментам.

Первым делом записываем трафик абонента в файл:

tcpdump -w bad.traf host 10.1.1.1

В таком случае tcpdump сохраняет трафик в PCAP-формате, который можно потом воспроизвести либо через tcpdump -r, либо открыть в wireshark, либо подсунуть в генератор трафика, который воспроизведёт трафик в реальные пакеты, что может быть полезно на тестовом стенде.

Затем я открыл файл в wireshark и изучил статистику.

Трафик, как я и говорил, небольшой: два с половиной мегабита, около тысячи пакетов в секунду. Сделаем выборку статистики по источникам трафика:
11

Итак, мы видим огромное количество источников трафика, с которыми наш клиент обменялся менее чем десятью пакетами. И более половины всех пакетов составляют SYN-запросы на установление соединения на порт 6881. В них-то и кроется вся проблема.

Выясняется, что клиентская машина активно участвует в DHT-сети с помощью одного из популярных торрент-клиентов. И другие участники сети активно ломятся к нашему клиенту за обновлением хэш-ключей. За 200 секунд устанавливается 110 000 соединений, большую часть из которых клиент не успевает обработать и оставляет без ответа.

На уровне оборудования это выглядит так: для каждого запроса создаётся новая NAT-трансляция. Т.к. клиентская машина не отвечает на эти запросы в силу загруженности, соединение остаётся полуоткрытым и получает таймаут в 300 секунд согласно предустановкам IPFW.

Через некоторое время часть трансляций устаревает, и NAT instance начинает активно заниматься двумя делами: уничтожением устаревших трансляций и созданием новых. Так как реализация libalias не предполагает размещение NAT Instance в отдельном ядре при большом количестве nat-конфигураций, нагрузка распределяется на все ядра.

Как бороться

1. Как оказалось, без явного указания флага deny_in в конфигурации NAT, входящий пакет будет обработан, даже если это соединение не было инициировано клиентом.  Исходный код libalias можно посмотреть в /usr/src/sys/netinet/libalias/, он хорошо документирован и понятен. Если никто из клиентов не просил соединения с хостом, а пакет всё равно пришёл, libalias будет выяснять, кто последний общался с этим хостом или кто последний принимал сообщения на этот порт. Этот клиент и получит пакет.
Первое, что надо сделать — это повесить deny_in на все NAT instance, где не требуется обрабатывать входящие соединения

2. Если отключить входящие соединения нельзя, можно их  ограничить по скорости.
Один SYN-пакет 62 байт или 496 бит. Значит очередь в 10 килобит\с позволит создавать 20 трансляций в секунду или 6 000 трансляций за 300 секунд (время nat-таймаута для полуоткрытой сессии ) что приемлемо для нашей конфигурации и не нарушит нормального сервиса для клиента.

ipfw pipe 666 config bw 30Kbit/s queue 2 mask dst-ip 0xffffffff
ipfw add 11 pipe 666 tcp from any to «table(72)» dst-port 6881 in recv vlan158 tcpflags syn

Экспериментально можно подобрать оптимальную ёмкость пайпа для ограничения вообще всех syn-пакетов клиенту, что защитит от syn-флуда на любой порт.

3. Ряд коллег решают эту проблему закрытием трафика по порту 6881:
ipfw add 11 deny tcp from any to «table(72)» dst-port 6881 in recv vlan158

Я же считаю, что это сомнительное решение: в таком случае syn-flood на любой другой порт положит сервер.

4. Ограничивать максимальное количество трансляций одному клиенту.
К сожалению, libalias не имеет такой настройки и количество трансляций ограничено лишь оперативной памятью. Но  при острой необходимости сделать соответствующий патч будет не сложно: созданием трансляций занимается процедура AddLink в файле /usr/src/sys/netinet/libalias/alias_db.c. В первых строчках этой процедуры можно добавить примерно такой код:
int tot = la->icmpLinkCount + la->udpLinkCount +
(la->sctpLinkCount>>1) + /* sctp counts half associations */
la->tcpLinkCount + la->pptpLinkCount +
la->protoLinkCount + la->fragmentIdLinkCount +
la->fragmentPtrLinkCount;
if (tot > 30000) // где 30 000 — это максимальное количество трансляций для одного NAT Instance
{
fprintf(stderr, » maximum number of translations exceeded\n»);
return (NULL);
}

Мониторинг
Если вы еще не сделали этого, рекомендую добавить в конфигурацию NAT директиву log, что позволит собирать статистику по количеству NAT-трансляций в системе.
Сделать это в командной строке можно будет так:
ipfw nat show | awk ‘{print $12;}’ | awk -F= ‘{print $2;}’ | sort -g

Эту информацию можно загнать в вашу систему мониторинга и отслеживать суммарное количество NAT-трансляций на сервере, а также максимальное количество NAT-трансляций у одного клиента. Внезапный рост числа трансляций относительно нормального состояния должен вызывать у вас приступ паники и беспокойства.

 

Comments

Об авторе

Василий Густелев
Заместитель Директора по техническим вопросам ООО «ПиП», г. Урай