Как пропатчить K̶D̶E̶ TCP-стек под FreeBSD

Habrahabr 1

Когда стоит вопрос выбора между проприетарным и открытым программным обеспечением, часто в пользу последнего приводят следующий аргумент: в случае необходимости всегда можно взять исходники и поправить их под себя, или исправить ошибку прямо сейчас, а не дожидаясь месяцами реакции от вендора. На самом деле этот аргумент весьма умозрительный — ну право же, кто в здравом уме возьмется за оптимизацию sql-планировщика, когда проще исправить сам sql запрос. Равно как и вряд-ли кто-то начнет искать и исправлять проблему в драйвере, когда быстрее и проще просто сменить железку. Баг-репорт отписать и то не всякий возьмется… Тем не менее, бывают случаи, когда именно наличие открытого кода позволяет избежать потенциальных убытков в случае возникновения непредвиденных проблем. Об одном из таких я и хочу сейчас расказать.

Этот вечер пятницы не предвещал никаких проблем. Впереди были очередные выходные, планов особых не было, предполагалось спокойно отдохнуть ;) Но реальные события оказались куда интереснее предполагаемых…

Первый звоночек зазвенел в субботу поздним вечером. Я уже спал, но пришлось подниматься и, чертыхаясь, идти разбираться, почему слег один из важных серверов. Их в кластере стояло 3+3 штук, и каждый тянет всю нагрузку своей тройки, так что выпадение одного ничем сервису не грозило. Но все же было крайне неприятно осознавать тот факт, что сервера, доселе спокойно принимающие суммарный входящий трафик в 10+К http-запросов в секунду, и имевшие (как казалось) ещё несколько-кратный запас по производительности, вдруг оказались не такими уж и устойчивыми. Что же, пока ребилдился raid1 и постгрес догонял репликацию, было время посмотреть на остальные сервера.

Стоит заранее объяснить, как устроен данный кластер. Сервера стоят в разных местах, два в Европе и четыре в USA. Они разбиты на тройки, каждая обслуживает свою группу IP (т.е. для каждой тройки один сервер в Европе и остальные два в USA). Трафик распределяется средствами anycast — на всех серверах тройки прописаны одни и те же IP, и поднята BGP-сессия с роутером. Если один сервер ложится, то его роутер перестает анонсировать его сеть в Internet и трафик автоматически уходит на оставшиеся сервера.

Смотреть особо было не на что. По данным мониторилки непосредственно перед падением был сильный всплеск входящего и исходящего трафика на оба европейских сервера (один из них и слег), причем если бэндвич вырос просто в два раза, то кол-во пакетов в секунду стало бо ́льшим уже раз в десять, причем в обе стороны. Т.е. пакеты были мелкие, и их было много (под 200к в секунду). На highload сервисах трафик просто-так сам по себе не меняется, а тут ещё и в таких размерах… Очень похоже на DDOS, не так ли? Не сказать что я сильно удивился, DDOS-ов разных видов мне пришлось повидать немало, и пока что, если сетевое оборудование у провайдеров позволяло доставлять трафик без потерь на сервера, их все удавалось успешно блокировать. Удивляло, правда, то, что всплеск трафика был только на европейских серверах, ведь если ботнет распределенный, то и трафик должен быть тоже распределен на весь кластер.

После ввода сервера в строй я запустил `top`, `nload` и стал мониторить загрузку. Ждать пришлось недолго, трафик скоро опять поднялся в два раза и ssh сессия начала ощутимо лагать. Налицо потери пакетов, `mtr -ni 0.1 8.8.8.8` данную гипотезу сразу и подтвердил, а `top -SH` указал, что дело именно в ядре ОС — обработчику входящих сетевых пакетов не хватает CPU. Что же, теперь понятно, почему завис сервер — потери пакетов ему смерти подобны:

У FreeBSD есть одна весьма неприятная особенность в сетевом стеке — он плохо масштабируется относительно кол-ва TCP-сессий :(. Увеличение кол-ва TCP-сессий в несколько раз приводит к непропорционально большему потреблению CPU. Пока сессий немного, то проблем нет, но начиная с нескольких десятков тысяч активных TCP-сессий обработчик входящих пакетов начинает испытывать нехватку CPU и ему приходится дропать пакеты. А это приводит к цепной реакции — из-за потерь пакетов активные TCP-сессии начинают медленно обслуживаться, их кол-во начинает немедленно расти, а с ним растет и нехватка CPU и уровень потерь пакетов.

Пока сервер окончательно не завис, срочно тушу BGP-сессию, и параллельно запускаю проверку потерь пакетов на том сервере, что принял на себя европейский трафик. Он имеет несколько более мощное железо — есть шансы, что в Штатах ничего плохого не случится. Но с проблемным сервером надо срочно решать. Первым делом выключаю keep-alive — TCP-сессии начнут завершаться раньше и в сумме их будет уже меньше. Тюнинг настроек сетевой карты занял не один десяток минут, проверяя наличие потерь пакетов каждый раз кратковременным поднятием BGP-сессии — пришлось оставить polling режим, но активировать idlepoll — теперь одно ядро процессора было занято исключительно сетевой картой, но зато потери пакетов прекратились.

Оставались еще непонятные моменты — например, кол-во TCP-сессий во время атаки и в обычном рабочем режиме практически не отличалось. Но вот что было совершенно непонятно, так это почему на штатовских серверах этой атаки не было видно вообще! Во время отключения европейских серверов на штатовские приходил только актуальный рабочий трафик, но не было никакого дополнительного! Хотя после возвращения трафика в Европу он какое-то время держался на рабочем уровне, а потом начинался очередной всплеск.

Время было за час ночи, потери пакетов, кажется, удалось прекратить, а с этими сетевыми странностями можно разбираться и на свежую голову. С такими мыслями я отправился обратно спать, хотя выспаться мне в ту ночь было не суждено. Через пару часов меня опять разбудили — в этот раз лежали уже оба европейских сервера ;(. Что добавило очередную странность в копилку — ведь время уже было позднее, и пик трафика был давно позади. Хотя, как для DDOS-атаки так вполне нормально, ведь большинство специалистов спит и заниматься атакой есть мало кому ). Оба сервера были вскоре запущены, но последующий мониторинг ситуации ничего нового не дал — атака в тот день больше не повторялась.

В воскресенье пришлось немного поработать ). Отдельный скрипт уже мониторил кол-во TCP-сессий и временно снимал трафик (т.е. переводил его в Штаты) в случае повышенной нагрузки, что уменьшало полученный ущерб. Пока что штатовские сервера работали без проблем, но все же надо было разобраться с этим трафиком и научиться его блокировать. В http-логах никаких аномалий не было, netstat и подобные утилиты тоже ничего подозрительного не показывали. Но раз мы видим повышение трафика на сетевой карте, то с этим уже можно работать, а на помощь придет верный tcpdump )

Пролистывать тонны дампов сетевых пакетов бывает непросто, но в этот раз долго искать не пришлось — среди обычного HTTP/HTTPS обмена было видно аномально много пустых TCP-пакетов, т.е. легальных пакетов с корректными IP и TCP заголовками, но без данных. При выключеном keep-alive пустых пакетов и так немало — три пустых на установление соединения, потом два пакета обмена данными, и потом опять пустые пакеты закрывают соединения. Ну и для HTTPS у нас ещё есть пакеты с данными для установки TLS-сессии. Но вот сейчас в дампе регулярно видны интенсивные обмены пустыми пакетами: 13:48:20.229921 IP 103.248.114.6.49467 > 88.208.9.69.80: Flags [.], ack 1, win 0, length 0
13:48:20.229925 IP 88.208.9.69.80 > 103.248.114.6.49467: Flags [.], ack 4294966738, win 8400, length 0
13:48:20.229927 IP 103.248.114.6.49467 > 88.208.9.69.80: Flags [.], ack 1, win 0, length 0
13:48:20.229931 IP 88.208.9.69.80 > 103.248.114.6.49467: Flags [.], ack 4294966738, win 8400, length 0
13:48:20.229933 IP 103.248.114.6.49467 > 88.208.9.69.80: Flags [.], ack 1, win 0, length 0
13:48:20.229937 IP 88.208.9.69.80 > 103.248.114.6.49467: Flags [.], ack 4294966738, win 8400, length 0
13:48:20.229939 IP 103.248.114.6.49467 > 88.208.9.69.80: Flags [.], ack 1, win 0, length 0
Выборочная проверка (`tcpdump -nc 1000 host 103.248.114.6 and tcp port 49467`) отдельных TCP-сессий показала, что таки да, по некоторым сессиям происходит очень интенсивный обмен пустыми TCP-пакетами. Причем почти все эти сессии были родом из Индии! Было и немного Саудовской Аравии с Кувейтом. Сложно сказать, что это за такой хитрый ботнет, да пока и не до этого. Пишу второй несложный скрипт, который каждую секунду запускает tcpdump на 30к пакетов и ищет среди них сессии, в которых кол-во последовательных обменов пустыми пакетами превышает указанный лимит, найденные IP немедленно блокируются. Результат не заставил себя ждать — при блокировке только пяти IP трафик тут же падает в два раза. Каждую минуту блокировался ещё один-два новых IP. Победа! ))

В понедельник с коллегами обсудили эту проблему, оказалось что все не так радужно (. Во первых, интенсивность блокировки новых IP нарастала — уже не в пик трафика скорость блокировки доходила до нескольких десятков штук в минуту. Во вторых, затронутыми оказались не только эти сервера, но и много других. Что характерно, все в Европе и все на FreeBSD. Стало ясно, что это никакая не DDOS-атака. Вот только что же это?…

Пока суть да дело, а заблокированные IP надо освободить. Вместо блокировки теперь дропались сами TCP-сессии (во FreeBSD для этого есть утилита tcpdrop). Это так же эффективно удерживало нагрузку под контролем. Заодно и keep-alive можно включить.

Пришлось опять брать в руки tcpdump и смотреть трафик дальше. Не буду детально описывать те часы, которые были потрачены на поиск аномалий и закономерностей, рассказ и так уже весьма затянулся ). TCP-сессии были разные. Были и полностью пустые:

dump1
06:07:58.753852 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [S], seq 3258188889, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
06:07:58.753868 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [S.], seq 2165986257, ack 3258188890, win 8192, options [mss 1452,nop,wscale 6,sackOK,eol], length 0
06:07:58.906312 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [S], seq 3258188889, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
06:07:58.906327 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [S.], seq 2165986257, ack 3258188890, win 8192, options [mss 1452,nop,wscale 6,sackOK,eol], length 0
06:07:59.059091 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [S], seq 3258188889, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
06:07:59.059103 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [S.], seq 2165986257, ack 3258188890, win 8192, options [mss 1452,nop,wscale 6,sackOK,eol], length 0
06:07:59.112677 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, length 0
06:07:59.161950 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, options [nop,nop,sack 1 {0:1}], length 0
06:07:59.269749 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, length 0
06:07:59.313826 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, options [nop,nop,sack 1 {0:1}], length 0
06:08:09.313764 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [.], ack 1, win 136, length 0
06:08:09.569443 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, length 0
06:08:09.678113 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [F.], seq 1, ack 1, win 260, length 0
06:08:09.678132 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [.], ack 2, win 136, length 0
06:08:09.678206 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [F.], seq 1, ack 2, win 136, length 0
06:08:09.720977 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, length 0
06:08:09.872479 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, length 0
06:08:09.932997 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 2, win 260, length 0
06:08:10.024179 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 1, win 260, length 0
06:08:20.023725 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [.], ack 1, win 8712, length 0
06:08:20.279407 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 2, win 0, length 0
06:08:20.279412 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [.], ack 1, win 8712, length 0
06:08:20.430575 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 2, win 0, length 0
06:08:20.430581 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [.], ack 1, win 8712, length 0
06:08:20.534901 IP 122.167.126.199.56698 > 88.208.9.8.80: Flags [.], ack 2, win 0, length 0
06:08:20.534908 IP 88.208.9.8.80 > 122.167.126.199.56698: Flags [.], ack 1, win 8712, length 0
а были и с обменом данными, которые потом переходили в цикл обмена пустыми пакетами:
dump2
06:18:39.046506 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [S], seq 1608423399, win 14600, options [mss 1400,sackOK,TS val 2790685 ecr 0,nop,wscale 6], length 0
06:18:39.046525 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [S.], seq 3258835787, ack 1608423400, win 8192, options [mss 1400,nop,wscale 6,sackOK,TS val 2982841058 ecr 2790685], length 0
06:18:39.228192 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 2790704 ecr 2982841058], length 0
06:18:39.234683 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [P.], seq 1:512, ack 1, win 229, options [nop,nop,TS val 2790704 ecr 2982841058], length 511
06:18:39.235039 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [P.], seq 1:358, ack 512, win 130, options [nop,nop,TS val 2982841246 ecr 2790704], length 357
06:18:39.379057 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 2790704 ecr 2982841058], length 0
06:18:39.385527 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [P.], seq 1:512, ack 1, win 229, options [nop,nop,TS val 2790704 ecr 2982841058], length 511
06:18:39.408290 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 358, win 274, options [nop,nop,TS val 2790722 ecr 2982841246], length 0
06:18:39.408304 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 130, options [nop,nop,TS val 2982841420 ecr 2790722], length 0
06:18:39.408305 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [F.], seq 512, ack 358, win 274, options [nop,nop,TS val 2790722 ecr 2982841246], length 0
06:18:39.408312 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 513, win 130, options [nop,nop,TS val 2982841420 ecr 2790722], length 0
06:18:39.408319 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [F.], seq 358, ack 513, win 130, options [nop,nop,TS val 2982841420 ecr 2790722], length 0
06:18:39.536434 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [P.], seq 1:512, ack 1, win 229, options [nop,nop,TS val 2790704 ecr 2982841058], length 511
06:18:39.536442 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [F.], seq 358, ack 513, win 130, options [nop,nop,TS val 2982841548 ecr 2790722], length 0
06:18:39.580158 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790739 ecr 2982841420], length 0
06:18:39.580167 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790739 ecr 2982841420], length 0
06:18:39.687698 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [P.], seq 1:512, ack 1, win 229, options [nop,nop,TS val 2790704 ecr 2982841058], length 511
06:18:39.688031 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [P.], seq 1:358, ack 512, win 138, options [nop,nop,TS val 2982841058 ecr 2790704], length 357
06:18:39.712200 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790752 ecr 2982841420], length 0
06:18:39.712204 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841083 ecr 2790704], length 0
06:18:39.882468 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790769 ecr 2982841420], length 0
06:18:39.882476 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841253 ecr 2790704], length 0
06:18:39.884164 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790769 ecr 2982841420], length 0
06:18:39.884170 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841255 ecr 2790704], length 0
06:18:39.917773 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [P.], seq 1:358, ack 512, win 138, options [nop,nop,TS val 2982841289 ecr 2790704], length 357
06:18:40.033516 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790769 ecr 2982841420], length 0
06:18:40.033525 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841404 ecr 2790704], length 0
06:18:40.035244 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790769 ecr 2982841420], length 0
06:18:40.035248 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841406 ecr 2790704], length 0
06:18:40.082506 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790789 ecr 2982841420], length 0
06:18:40.082513 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841453 ecr 2790704], length 0
06:18:40.132575 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790794 ecr 2982841420], length 0
06:18:40.132583 IP 88.208.9.8.80 > 106.193.154.239.1223: Flags [.], ack 512, win 138, options [nop,nop,TS val 2982841503 ecr 2790704], length 0
06:18:40.142588 IP 106.193.154.239.1223 > 88.208.9.8.80: Flags [.], ack 359, win 274, options [nop,nop,TS val 2790795 ecr 2982841420], length 0

Но зацепка все же была. Перед уходом в цикл обмена пустыми пакетами от удаленной стороны приходил FIN пакет (пакет с флагом FIN сигнализирует, что данных больше не будет и сессию надо закрывать), иногда и не один, а бывало и RST пакет (пакет с флагом RST указывает, что сессия уже закрыта и больше не валидна). Что интересно, несмотря на наличие FIN и RST пакетов, потом бывало что на сервер приходили и пакеты с данными. Либо где-то настолько криво реализован TCP-стек, что маловероятно, либо где-то происходит грубое вмешательство в TCP-сессии, а вот это уже вполне вероятно (особенно этим любят баловаться мобильные операторы, не буду показывать пальцем). Вторую версию также подтверждал тот факт, что проверка по http-логу найденных зловредных TCP-сессий показала, что практически во всех них был задействован мобильный браузер, причем как Android, так и iOS.

Логично было предположить, FIN или RST пакет переводил TCP-сессию в закрытое состояние, в котором TCP-стек просто подтверждал получение пакетов. Было интересно, какое конкретно из TCP-состояний

tcp_fsm.h
#define TCP_NSTATES     11

#define TCPS_CLOSED             0       /* closed */
#define TCPS_LISTEN             1       /* listening for connection */
#define TCPS_SYN_SENT           2       /* active, have sent syn */
#define TCPS_SYN_RECEIVED       3       /* have sent and received syn */
/* states < TCPS_ESTABLISHED are those where connections not established */
#define TCPS_ESTABLISHED        4       /* established */
#define TCPS_CLOSE_WAIT         5       /* rcvd fin, waiting for close */
/* states > TCPS_CLOSE_WAIT are those where user has closed */
#define TCPS_FIN_WAIT_1         6       /* have closed, sent fin */
#define TCPS_CLOSING            7       /* closed xchd FIN; await FIN ACK */
#define TCPS_LAST_ACK           8       /* had fin and close; await FIN ACK */
/* states > TCPS_CLOSE_WAIT && < TCPS_FIN_WAIT_2 await ACK of FIN */
#define TCPS_FIN_WAIT_2         9       /* have closed, fin is acked */
#define TCPS_TIME_WAIT          10      /* in 2*msl quiet wait after close */

так себя ведет, и перед вызовом `tcpdrop` я добавил поиск удаляемой TCP-сесии в выводе `netstat -an`. Результат был немного обескураживающим — они все были в состоянии ESTABLISHED! Это уже было сильно похоже на баг — не может закрытая TCP-сессия перейти обратно в состояние ESTABLISHED, не предусмотрен такой вариант. Я немедленно начал проверять исходники и ядра и был обескуражен второй раз:
tp->t_state = TCPS_ESTABLISHED
в коде вызывается ровно два раза, и оба раза непосредственно перед этим проверяется текущее значение t_state — в одном случае оно равно TCPS_SYN_SENT (сервер отослал SYN пакет и получил подтверждение), а во втором это TCPS_SYN_RECEIVED (сервер получил SYN, отправил SYN/ACK и получил подтверждающий ACK). Вывод из это следует вполне конкретный — FIN и RST пакеты сервером игнорировались, и никакого бага в TCP-стеке нет (по крайней мере, бага с неправильным переходом из одного состояния в другое).

Все же было непонятно, зачем серверу отвечать на каждый полученный TCP пакет. Обычно в этом нет необходимости, и TCP-стек работает по другому — он принимает несколько пакетов, а потом отсылает одним пакетом подтверждение для всех сразу — так экономнее ). Пролить свет на ситуацию помогло внимательное изучение содержимого пакетов, в частности 32-х битных счетчиков TCP — sequence и acknowledgement. Поведение tcpdump по умолчанию — показывать разницу seq/ack между пакетами вместо абсолютных значений в данном случае сыграло дурную службу :).

Посмотрим внимательно на абсолютные значения: 16:03:21.931367 IP (tos 0x28, ttl 47, id 44771, offset 0, flags [DF], proto TCP (6), length 60)
46.153.19.182.54645 > 88.208.9.111.80: Flags [S], cksum 0x181c (correct), seq 3834615051, win 65535, options [mss 1460,sackOK,TS val 932840 ecr 0,nop,wscale 6], length 0
16:03:21.931387 IP (tos 0x0, ttl 64, id 1432, offset 0, flags [DF], proto TCP (6), length 60)
88.208.9.111.80 > 46.153.19.182.54645: Flags [S.], cksum 0xa4bc (incorrect -> 0xf9a4), seq 1594895211, ack 3834615052, win 8192, options [mss 1460,nop,wscale 6,sackOK,TS val 2509954639 ecr 932840], length 0
16:03:22.049434 IP (tos 0x28, ttl 47, id 44772, offset 0, flags [DF], proto TCP (6), length 52)
46.153.19.182.54645 > 88.208.9.111.80: Flags [.], cksum 0x430b (correct), seq 3834615052, ack 1594895212, win 1369, options [nop,nop,TS val 932852 ecr 2509954639], length 0
16:03:22.053697 IP (tos 0x28, ttl 47, id 44773, offset 0, flags [DF], proto TCP (6), length 40)
46.153.19.182.54645 > 88.208.9.111.80: Flags [R], cksum 0x93ba (correct), seq 211128292, win 1369, length 0
16:03:22.059913 IP (tos 0x28, ttl 48, id 0, offset 0, flags [DF], proto TCP (6), length 40)
46.153.19.182.54645 > 88.208.9.111.80: Flags [R.], cksum 0xa03f (correct), seq 0, ack 1594897965, win 0, length 0
16:03:22.060700 IP (tos 0x28, ttl 47, id 44774, offset 0, flags [DF], proto TCP (6), length 52)
46.153.19.182.54645 > 88.208.9.111.80: Flags [.], cksum 0x3a48 (correct), seq 3834615953, ack 1594896512, win 1410, options [nop,nop,TS val 932853 ecr 2509954639], length 0
16:03:22.060706 IP (tos 0x0, ttl 64, id 3974, offset 0, flags [DF], proto TCP (6), length 52)
88.208.9.111.80 > 46.153.19.182.54645: Flags [.], cksum 0xa4b4 (incorrect -> 0x475c), seq 1594895212, ack 3834615052, win 135, options [nop,nop,TS val 2509954768 ecr 932852], length 0
Первый пакет содержит seq 3834615051, в ответ от сервера ушел пакет seq 1594895211, ack 3834615052 (в out-ack ушел номер in-seq + 1) Потом пришло пару RST пакетов, они нам не интересны. А вот следующий пакет нам интересен — в нем записаны номера seq 3834615953, ack 1594896512. Оба эти номера существенно больше чем initial seq/ack, а это означает, что удаленная сторона уже отослала 3834615953-3834615052=901 байт и даже успела получить 1594896512-1594895212=1300 байт. Разумеется, этих пакетов с данными мы не видим и не увидим — этот обмен был с MiTM системой. Но сервер то этого не знает. Он видит пакет с seq 3834615953, а следовательно, что ему не пришло 901 байт данных, и отсылает обратно пакет с последними валидными seq/ack номерами, ему известными — seq 1594895212, ack 3834615052. Удаленная сторона получает этот пакет, и в свою очередь рапортует, что у неё все отлично, 1300 байт данных получены успешно. Вот у нас и зацикливание.

Также становится понятным, почему штатовские сервера не видели этого трафика — он на самом деле был, но во много раз меньше — во столько же раз, во сколько пинг из Индии в Штаты больше, чем пинг из Индии в Европу.

Осталось, собственно, найти, как исправить этот баг. Опять берем исходники, интересующий нас код находится в файле tcp_input.c. Это было не особо сложно — первичной обработкой TCP-пакета занимается функция tcp_input() — в самом конце, если пакет проходит все проверки и TCP-соединение находится в состоянии ESTABLISHED — пакет передается на обработку в функцию tcp_do_segment(). Надо просто добавить ещё одну проверку — если ack-счетчик от удаленной стороны показывает, что она получила данные, которые сервер не отсылал — пакет надо игнорировать. Обрывать сразу соединение нельзя — иначе мы откроем злоумышленникам простой способ обрывать чужие TCP-соединения ). Тестирование патча показало, что в TCP-трафике также присутствуют пакеты с нулевым значением ack — их игнорировать уже не надо. Итоговый патч занял три строчки (без учета комментариев):

+        if(SEQ_GT(th->th_ack, tp->snd_max) && th->th_ack != 0) {
+               goto dropunlock;
+       }

PR (problem report) разработчикам FreeBSD отправлен в тот же день: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=219991

P.S. Как с этой проблемой обстоят дела в Linux и Windows? А там все нормально, такие пакеты игнорируются (тестировал Windows 10 и Linux 3.10).