大家好! 我叫 Dmitry Samsonov,是 Odnoklassniki 的首席系统管理员。 我们拥有 7 多台物理服务器、云中 11 个容器和 200 个应用程序,这些应用程序以不同的配置形成 700 个不同的集群。 绝大多数服务器运行 CentOS 7。
14 августа 2018 г. была опубликована информация об уязвимости FragmentSmack
(CVE-2018-5391) 和 SegmentSmack (CVE-2018-5390)。 这些漏洞具有网络攻击向量且得分相当高 (7.5),可能会因资源耗尽 (CPU) 而导致拒绝服务 (DoS)。 当时还没有提出针对 FragmentSmack 的内核修复方案;而且,它的发布时间比有关漏洞的信息发布要晚得多。 为了消除SegmentSmack,建议更新内核。 更新包本身已在同一天发布,剩下的就是安装它。
Нет, мы совсем не против обновления ядра! Однако есть нюансы…
Как мы обновляем ядро на проде
一般来说,没什么复杂的:
Cкачать пакеты;
将它们安装在许多服务器上(包括托管我们云的服务器);
确保没有任何损坏;
确保应用所有标准内核设置且没有错误;
等几天;
检查服务器性能;
Переключить деплой новых серверов на новое ядро;
Обновить все серверы по дата-центрам (один дата-центр за раз, чтобы минимизировать эффект для пользователей в случае проблем);
正如您可能已经猜到的那样,重新启动数千台服务器需要花费最长的时间。 由于并非所有漏洞都对所有服务器都至关重要,因此我们仅重新启动那些可直接从 Internet 访问的服务器。 在云中,为了不限制灵活性,我们不会将外部可访问的容器与具有新内核的各个服务器绑定在一起,而是毫无例外地重新启动所有主机。 幸运的是,那里的程序比常规服务器更简单。 例如,无状态容器可以在重新启动期间简单地移动到另一台服务器。
不过,工作量仍然很大,可能需要几周的时间,如果新版本出现任何问题,最多可能需要几个月的时间。 攻击者非常了解这一点,因此他们需要 B 计划。
FragmentSmack/SegmentSmack。 解决方法
К счастью, для некоторых уязвимостей такой план «Б» существует, и называется он Workaround. Чаще всего это изменение настроек ядра/приложений, которые позволяют минимизировать возможный эффект или полностью исключить эксплуатацию уязвимостей.
«Можно изменить дефолтные значения 4MB и 3MB в net.ipv4.ipfrag_high_thresh и net.ipv4.ipfrag_low_thresh (и их аналоги для ipv6 net.ipv6.ipfrag_high_thresh и net.ipv6.ipfrag_low_thresh) на 256 kB и 192 kB соответственно или ниже. Тесты показывают от небольшого до значительного падения использования CPU во время атаки в зависимости от оборудования, настроек и условий. Однако может быть некоторое влияние на производительность из-за ipfrag_high_thresh=262144 bytes, так как только два 64K-фрагмента могут одновременно уместиться в очереди на пересборку. Например, есть риск, что приложения, работающие с большими UDP-пакетами, поломаются“。
ipfrag_high_thresh - LONG INTEGER
Maximum memory used to reassemble IP fragments.
ipfrag_low_thresh - LONG INTEGER
Maximum memory used to reassemble IP fragments before the kernel
begins to remove incomplete fragment queues to free up resources.
The kernel still accepts new fragments for defragmentation.
我们在生产服务上没有大型 UDP。 LAN 上没有碎片流量;WAN 上有碎片流量,但不严重。 没有任何迹象 - 您可以推出解决方法!
FragmentSmack/SegmentSmack。 第一滴血
Первая проблема, с которой мы столкнулись, заключалась в том, что облачные контейнеры порой применяли новые настройки лишь частично (только ipfrag_low_thresh), а иногда не применяли вообще — просто падали на старте. Стабильно воспроизвести проблему не удавалось (вручную все настройки применялись без каких-либо сложностей). Понять, почему падает контейнер на старте, тоже не так-то просто: никаких ошибок не обнаружено. Одно было известно точно: откат настроек решает проблему с падением контейнеров.
Почему недостаточно применить Sysctl на хосте? Контейнер живёт в своём выделенном сетевом Namespace, поэтому по крайней мере 部分网络Sysctl参数 в контейнере может отличаться от хоста.
Как именно применяются настройки Sysctl в контейнере? Так как контейнеры у нас непривилегированные, изменить любую настройку Sysctl, зайдя в сам контейнер, не получится — просто не хватит прав. Для запуска контейнеров наше облако на тот момент использовало Docker (сейчас уже 波德曼). Докеру через API передавались параметры нового контейнера, в том числе нужные настройки Sysctl.
在搜索版本时,发现 Docker API 并未返回所有错误(至少在 1.10 版本中)。 当我们尝试通过“docker run”启动容器时,我们终于看到了至少一些东西:
write /proc/sys/net/ipv4/ipfrag_high_thresh: invalid argument docker: Error response from daemon: Cannot start container <...>: [9] System error: could not synchronise with container process.
首先,我们当然尝试回滚Sysctl设置,但这没有任何效果。 对服务器和应用程序设置的各种操作也没有帮助。 重新启动有帮助。 重新启动 Linux 与过去 Windows 中的正常现象一样不自然。 不过,它确实有帮助,我们将其归因于在 Sysctl 中应用新设置时出现的“内核故障”。 本来是多么的轻率啊……
三周后,问题再次出现。 这些服务器的配置非常简单:Nginx 处于代理/平衡器模式。 流量不大。 新介绍性说明:客户端 504 错误的数量每天都在增加(网关超时). На графике показано число 504-х ошибок в день по этому сервису:
所有错误都与同一个后端有关 - 与云中的后端有关。 该后端的包片段的内存消耗图如下所示:
Это одно из самых ярких проявлений проблемы на графиках операционной системы. В облаке как раз в это же время была пофикшена другая сетевая проблема с настройками QoS (Traffic Control). На графике потребления памяти под фрагменты пакетов она выглядела точно так же:
Анализ всей доступной статистики и логов не приблизил нас к пониманию происходящего. Остро не хватало возможности воспроизвести проблему, чтобы «пощупать» конкретное соединение. Наконец, разработчикам на спецверсии приложения удалось добиться стабильного воспроизведения проблем на тестовом устройстве при подключении через Wi-Fi. Это стало прорывом в расследовании. Клиент подключался к Nginx, тот проксировал на бекенд, которым являлось наше приложение на Java.
问题的对话是这样的(在Nginx代理端修复):
客户端:请求接收有关下载文件的信息。
Java 服务器:响应。
客户端:使用文件进行 POST。
Java 服务器:错误。
Java-сервер при этом пишет в лог, что от клиента получено 0 байт данных, а Nginx-прокси — что запрос занял больше 30 секунд (30 секунд — это время таймаута у клиентского приложения). Почему же таймаут и почему 0 байт? С точки зрения HTTP всё работает так, как должно работать, но POST с файлом как будто пропадает из сети. Причём пропадает между клиентом и Nginx. Пришло время вооружиться Tcpdump! Но для начала надо понять конфигурацию сети. Nginx-прокси стоит за L3-балансировщиком 网络软件。 隧道用于将数据包从 L3 平衡器传送到服务器,服务器将其标头添加到数据包中:
При этом сеть на этот сервер приходит в виде Vlan-теггированного трафика, которое тоже добавляет свои поля в пакеты:
А ещё этот трафик может фрагментироваться (тот самый небольшой процент входящего фрагментированного трафика, о котором мы говорили при оценке рисков от Workaround), что тоже меняет содержание хидеров:
Пакет попадает на L3-балансировщик. Для корректной маршрутизации внутри дата-центра пакет инкапсулируется в туннель и отправляется на сетевую карту.
由于数据包 + 隧道标头不适合 MTU,因此数据包被切割成片段并发送到网络。
L3 平衡器之后的交换机在收到数据包时,会为其添加 Vlan 标记并继续发送。
Свитч перед Nginx-прокси видит (по настройкам порта), что сервер ожидает Vlan-инкапсулированный пакет, поэтому отправляет его, как есть, не убирая Vlan-тег.
Linux получает фрагменты отдельных пакетов и склеивает их в один большой пакет.
Далее пакет попадает на Vlan-интерфейс, где с него снимается первый слой — Vlan-инкапсулирование.
0xx390x2xx 是十六进制格式的客户端 IP 地址。
32:4 — 隧道数据包中写入 SCR IP 的字段的地址和长度。
Адрес поля пришлось подбирать перебором, так как в интернете пишут про 40, 44, 50, 54, но там IP-адреса не было. Также можно посмотреть один из пакетов в hex (параметр -xx или -XX в tcpdump) и посчитать, по какому адресу известный вам IP.
是否存在未去除Vlan和Tunnel封装的报文分片?
tcpdump ((ip[6:2] > 0) and (not ip[6] = 64))
Эта магия покажет нам все фрагменты, включая последний. Наверное, то же можно зафильтровать по IP, но я не пытался, поскольку таких пакетов не очень много, и в общем потоке легко нашлись нужные мне. Вот они:
«The number of failures detected by the IP re-assembly algorithm (for whatever reason: timed out, errors, etc.)».
Среди группы серверов, на которых изучалась проблема, на двух этот счётчик увеличивался быстрее, на двух — медленнее, а ещё на двух вообще не увеличивался. Сравнение динамики этого счётчика с динамикой HTTP-ошибок на Java-сервере выявило корреляцию. То есть счётчик можно было ставить на мониторинг.
Наличие надёжного индикатора проблем очень важно, чтобы можно было точно определить, помогает ли откат Sysctl, так как из предыдущего рассказа мы знаем, что по приложению это сразу понять нельзя. Данный индикатор позволил бы выявить все проблемные места в продакшене до того, как это обнаружат пользователи.
После отката Sysctl ошибки по мониторингу прекратились, таким образом причина проблем была доказана, как и то, что откат помогает.
Мы откатили настройки фрагментации на других серверах, где загорелся новый мониторинг, а где-то под фрагменты выделили даже больше памяти, чем было до этого по умолчанию (это была udp-статистика, частичная потеря которой не была заметна на общем фоне).
最重要的问题
为什么数据包在我们的 L3 平衡器上会出现碎片? 从用户到达平衡器的大多数数据包都是 SYN 和 ACK。 这些封装的尺寸很小。 但由于此类数据包的份额非常大,因此在其背景下,我们没有注意到开始分段的大数据包的存在。
Причиной стал поломавшийся скрипт конфигурации advmss 在具有 Vlan 接口的服务器上(当时生产中很少有带有标记流量的服务器)。 Advmss 允许我们向客户端传达这样的信息:我们方向的数据包大小应该更小,以便在将隧道标头附加到它们之后,它们不必被分段。
Почему откат Sysctl не помогал, а ребут помогал? Откат Sysctl менял объём памяти, доступной для склеивания пакетов. При этом, судя по всему сам факт переполнения памяти под фрагменты приводил к торможению соединений, что приводило к тому, что фрагменты надолго задерживались в очереди. То есть процесс зацикливался.
重新启动会清除内存,一切恢复正常。
非常感谢安德烈·季莫费耶夫(阿蒂莫费耶夫) за помощь в проведении расследования, а также Алексею Кренёву (设备x) — за титанический труд по обновлению Centos и ядер на серверах. Процесс, который в данном случае несколько раз пришлось начинать с начала, из-за чего он затянулся на много месяцев.