
Здравейте на всички! Казвам се Дмитрий Самсонов, работя като водещ системен администратор в „Однокласниците“. Имаме над 7 хиляди физически сървъра, 11 хиляди контейнера в нашето облако и 200 приложения, които с различни конфигурации формират 700 различни клъстера. Поради това повечето от сървърите работят под управлението на CentOS 7.
На 14 август 2018 г. беше публикувана информация за уязвимостта FragmentSmack
() и SegmentSmack (). Това са уязвимости с мрежов вектор на атака и доста висока оценка (7.5), която заплашва с отказ на услуга (DoS) поради изчерпване на ресурсите (CPU). По онова време не беше предложен фикс за FragmentSmack в ядрото, и повече от това, той излезе значително по-късно след публикуването на информацията за уязвимостта. За отстраняване на SegmentSmack се предлагаше да се обнови ядрото. Самият пакет с обновлението беше издаден в същия ден, оставаше само да бъде инсталиран.
Не, никак не сме против обновлението на ядрото! Въпреки това, има нюанси…
Как обновяваме ядрото на продукцията
Всъщност, нищо сложно:
- Свалете пакетите;
- Инсталирайте ги на определен брой сървъри (включително сървърите, хостващи нашето облако);
- Уверете се, че нищо не е счупено;
- Удостоверете се, че всички стандартни настройки на ядрото са приложени без грешки;
- Изчакайте няколко дни;
- Проверете показателите на сървърите;
- Превключете внедряването на новите сървъри на новото ядро;
- Обновете всички сървъри по датацентровете (един датацентър за един път, за да минимизирате ефекта за потребителите в случай на проблеми);
- Рестартирайте всички сървъри.
Повторете за всички версии на ядрата, които имаме. В момента това са:
- Стоковото CentOS 7 3.10 — за повечето обикновени сървъри;
- Ванилно 4.19 — за нашето , защото ни трябват BFQ, BBR и т.н.;
- Elrepo kernel-ml 5.2 — за , защото 4.19 по-рано се държеше нестабилно, а нужните функции са същите.
Както можете да се досетите, най-много време отнема рестартирането на хиляди сървъри. Тъй като не всички уязвимости са критични за всички сървъри, рестартиране на серверите извършваме само за тези, които са достъпни пряко от интернет. В облака, за да не ограничаваме гъвкавостта, не свързваме достъпните отвън контейнери с отделни сървъри с ново ядро, а рестартираме всички хостове без изключение. За щастие, там процедурата е по-лесна, отколкото с обикновените сървъри. Например, stateless-контейнерите могат просто да се преместят на друг сървър по време на ребута.
Въпреки това, работата е много и може да отнеме няколко седмици, а при възникване на проблеми с новата версия – дори няколко месеца. Злонамерените лица разбират това много добре, затова е необходим план „Б“.
FragmentSmack/SegmentSmack. Workaround
Щастливо, за някои уязвимости такъв план „Б“ съществува и се нарича Workaround. По-често това е промяна на настройките на ядрото/приложенията, които позволяват минимизиране на евентуалния ефект или напълно изключване на експлоатацията на уязвимости.
В случая с 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 - ДЪЛГО ЦЯЛО ЧИСЛО
Максималната памет, използвана за сглобяване на IP фрагменти.
ipfrag_low_thresh - ДЪЛГ ТОЧЕН
Максималната памет, използвана за възстановяване на IP фрагменти, преди ядрото
да започне да премахва непълните опашки от фрагменти, за да освободи ресурси.
Ядрото все още приема нови фрагменти за дефрагментация.
На продукционните услуги нямаме големи UDP. В LAN фрагментирания трафик отсъства, в WAN има, но не е значителен. Нищо не предвещава – можете да прилагате Workaround!
FragmentSmack/SegmentSmack. Първа кръв
Първият проблем, с който се сблъсквахме, беше, че облачните контейнери понякога прилагаха новите настройки само частично (само ipfrag_low_thresh), а понякога не ги прилагаха изобщо — просто се срутваха при стартиране. Успешното възпроизвеждане на проблема не беше възможно (всички настройки се прилагаха ръчно без никакви затруднения). Да разберем защо контейнерът пада при стартиране, също не е лесно: не бяха открити грешки. Едно беше сигурно: откатът на настройките решава проблема с падането на контейнерите.
Защо не е достатъчно да се приложи Sysctl на хоста? Контейнерът работи в своето отделно мрежово пространство, затова поне в контейнера може да се различава от хоста.
Как точно се прилагат настройките Sysctl в контейнера? Тъй като контейнерите ни не са привилегировани, не е възможно да се промени която и да е настройка Sysctl в самия контейнер — просто нямаме достатъчно права. За стартиране на контейнерите, нашето облако по това време използваше Docker (в момента вече ). На Docker чрез API се предаваха параметрите на новия контейнер, включително необходимите настройки Sysctl.
В хода на разглеждането на версиите установихме, че API-то на Docker не върна всички грешки (поне в версия 1.10). При опит да стартираме контейнер чрез “docker run” накрая видяхме нещо:
write /proc/sys/net/ipv4/ipfrag_high_thresh: невалиден аргумент docker: Отговор за грешка от демон: Не може да стартира контейнера <...>: [9] Системна грешка: не можа да синхронизира с процеса на контейнера.
Стойността на параметъра е невалидна. Но защо? И защо е невалидна само понякога? Установи се, че Docker не гарантира реда на прилагане на параметрите Sysctl (последно проверената версия е 1.13.1), затова понякога ipfrag_high_thresh се опитваше да се зададе на 256K, когато ipfrag_low_thresh все още беше 3M, т.е. горната граница беше по-ниска от долната, което и доведе до грешката.
По това време вече използвахме нашия механизъм за донастройка на контейнера след стартиране (замразяване на контейнера чрез и изпълнение на команди в пространството на контейнера чрез ), и добавихме в тази част също записване на параметрите Sysctl. Проблемът беше решен.
FragmentSmack/SegmentSmack. Първа кръв 2
Не успяхме да разберем прилагането на Workaround в облака, когато започнаха да постъпват първите редки оплаквания от потребители. По това време минаха няколко седмици от началото на прилагането на Workaround на първите сървъри. Първоначалното разследване показа, че оплакванията идват от отделни услуги, а не от всички сървъри на тези услуги. Проблемът отново придоби крайно неопределен характер.
На първо място, разбира се, опитахме да върнем настройките на Sysctl, но това не даде никакъв ефект. Различни манипулации с настройките на сървъра и приложението също не помогнаха. Помогна рестарт. Рестартът за Linux е толкова противоестествен, колкото беше нормално условие за работа с Windows в миналото. Въпреки това той помогна и списахме всичко на 'глюк в ядрото' при прилагането на новите настройки в Sysctl. Как беше лекомислено…
След три седмици проблемът се повтори. Конфигурацията на тези сървъри беше доста проста: Nginx в режим на прокси/балансировщик. Трафикът е малко. Новото е, че на клиентите с всеки изминал ден нараства броят на 504-те грешки (). На графика е показано броят на 504-те грешки на ден за тази услуга:

Всички грешки са за един и същи бекенд — този, който се намира в облака. Графикът за консумация на памет под фрагменти от пакети на този бекенд изглеждаше по следния начин:

Това е едно от най-ярките проявления на проблема на графиките на операционната система. В облака точно по това време беше поправен друг мрежови проблем с настройките на QoS (Traffic Control). На графика за консумация на памет под фрагменти от пакети той изглеждаше точно така:

Предположението беше просто: ако на графиките изглеждат еднакво, причината им също е една и съща. Особено след като подобни проблеми с този тип памет се случват изключително рядко.
Същността на поправения проблем беше, че използвахме в QoS пакетния шедулдер fq с настройките по подразбиране. По подразбиране за една връзка той позволява да се добавят 100 пакета в опашката и някои връзки в ситуация на недостиг на канал започнаха да запълват опашката до отказ. В такъв случай пакетите се дропват. В статистиката tc (tc -s qdisc) това изглежда по следния начин:
qdisc fq 2c6c: родител 1:2c6c лимит 10000p flow_limit 100p купчини 1024 orphan_mask 1023 квант 3028 начално_квант 15140 refill_delay 40.0ms
Изпратени 454701676345 байта 491683359 пакета (отхвърлени 464545, над лимита 0 повторни опити 0)
чернова 0b 0p повторни опити 0
1024 потока (1021 неактивни, 0 ограничени)
0 gc, 0 високприоритетни, 0 ограничени, 464545 flows_plimit
«464545 flows_plimit» — това са пакетите, които са били отхвърлени поради превишаване на лимита на опашката на едно свързване, а «dropped 464545» — това е сумата на всички отхвърлени пакети на този шедулер. След увеличаването на дължината на опашката до 1 000 и рестартиране на контейнерите, проблемът спря да се проявява. Може да се отпуснете в стола и да изпите смути.
FragmentSmack/SegmentSmack. Последната кръв
Първо, след няколко месеца след анонса на уязвимостите в ядрото, най-накрая се появи фикс за FragmentSmack (да напомня, че заедно с анонса през август излезе фикс само за SegmentSmack), което даде шанс да се откажем от Workaround, който ни донесе доста неприятности. Част от сървърите вече успяхме да преместим на новото ядро, и сега трябваше да започнем отначало. Защо обновявахме ядрото, без да изчакаме фикса за FragmentSmack? Причината е, че процесът на защита от тези уязвимости съвпадна (и се сля) с процеса на обновяване на самия CentOS (което отнема дори повече време от обновлението само на ядрото). Освен това, SegmentSmack е по-опасна уязвимост, а фикс за нея се появи веднага, така че имаше смисъл в какъвто и да е случай. Все пак, просто да обновим ядрото на CentOS не можехме, защото уязвимостта FragmentSmack, която се появи през времето на CentOS 7.5, беше пофикшена само във версия 7.6, затова трябваше да спрем обновлението до 7.5 и да започнем всичко отначало с обновление до 7.6. И такова нещо се случва.
Второ, при нас се върнаха редки жалби от потребители относно проблеми. Сега знаем със сигурност, че всички те са свързани с ъплоад на файлове от клиенти на някои от нашите сървъри. Минаваха през тези сървъри много малко ъплоади от общото количество.
Както помним от предишната история, откатът на Sysctl не помогна. Помогна Ребут, но временно.
Съществуващите подозрения относно Sysctl не бяха отстранени, но този път беше необходимо да съберем колкото се може повече информация. Също така много липсваха възможности за възпроизвеждане на проблема с ъплоадите от клиента, за да изучим по-точно какво се случва.
Анализът на наличната статистика и логовете не ни приближи до разбирането на ситуацията. Острано ни липсваше възможността да възпроизведем проблема, за да "пипнем" конкретната връзка. Накрая, на специална версия на приложението разработчиците успяха да постигнат стабилно възпроизвеждане на проблемите на тестово устройство при свързване чрез Wi-Fi. Това стана пробив в разследването. Клиентът се свърза с Nginx, който проксира на бекенда, който представляваше нашето приложение на Java.

Диалогът при проблеми беше такъв (записан от страната на Nginx-прокси):
- Клиент: запитване за получаване на информация за доизтегляне на файл.
- Java-сървър: отговор.
- Клиент: POST с файл.
- Java-сървър: грешка.
Java-сървърът в този случай пише в логовете, че е получено 0 байта данни от клиента, а Nginx-прокси — че запитването е отнело повече от 30 секунди (30 секунди е времето за таймаут на клиентското приложение). Защо таймаут и защо 0 байта? От гледна точка на HTTP всичко работи както трябва, но POST с файла сякаш изчезва от мрежата. При това изчезва между клиента и Nginx. Дойде време да се въоръжим с Tcpdump! Но първо трябва да разберем конфигурацията на мрежата. Nginx-прокси стои зад L3-балансировщика. . Използва се тунелиране за доставка на пакети от L3-балансировчика до сървъра, което добавя свои хидери в пакетите:

При това мрежата към този сървър пристига под формата на Vlan-теглиран трафик, който също добавя своите полета в пакетите:

А също и този трафик може да бъде фрагментиран (онзи малък процент влязъл фрагментиран трафик, който споменахме при оценката на рисковете от Workaround), което също променя съдържанието на хидерите:

Още веднъж: пакетите са инкапсулирани с Vlan-тег, инкапсулирани в тунел, фрагментиране. За да разбера как точно става това, ще проследим маршрута на пакета от клиента до Nginx-прокси.
- Пакетът попада на L3-балансировщика. За коректна маршрутизация в дата центъра пакетът се инкапсулира в тунел и се изпраща на мрежовата карта.
- Тъй като пакетът + хидерите на тунела не се побират в MTU, пакетът се разрязва на фрагменти и се изпраща в мрежата.
- Свитчът след L3-балансировчика при получаване на пакета добавя Vlan-тег и го изпраща напред.
- Свитчът пред Nginx-прокси вижда (по настройките на порта), че сървърът очаква пакет с Vlan инкапсулация, затова го изпраща, както е, без да премахва Vlan-тага.
- Linux получава фрагменти от отделни пакети и ги обединява в един голям пакет.
- След това пакетът попада на Vlan-интерфейса, където първият слой — Vlan инкапсулацията — се премахва.
- След това Linux го изпраща на Tunnel-интерфейса, където се премахва още един слой — Tunnel инкапсулацията.
Сложността е да предадем всичко това под формата на параметри към tcpdump.
Да започнем от края: има ли чисти (без излишни заглавия) IP пакети от клиенти, с премахната vlan и tunnel инкапсулация?
tcpdump host
Не, такива пакети на сървъра не е имало. Следователно, проблемът трябва да е по-рано. Има ли пакети с премахната само Vlan инкапсулация?
tcpdump ip[32:4]=0xx390x2xx
0xx390x2xx — това е IP адресът на клиента в hex формат.
32:4 — адресът и дължината на полето, в което е записан SCR IP в Tunnel пакета.
Адресът на полето беше подбиран на сляпо, тъй като в интернет пишат за 40, 44, 50, 54, но там не беше имало IP адреси. Може също да се погледне един от пакетите в hex (параметър -xx или -XX в tcpdump) и да се изчисли, по какъв адрес е известният ви IP.
Има ли фрагменти на пакети без премахната Vlan и Tunnel инкапсулация?
tcpdump ((ip[6:2] > 0) и (не ip[6] = 64))
Тази магия ще ни покаже всички фрагменти, включително и последния. Вероятно, и същото може да се филтрира по IP, но не съм опитвал, тъй като такива пакети не са много и в общия поток лесно намерих необходимите. Ето ги:
14:02:58.471063 В 00:de:ff:1a:94:11 етерал IPv4 (0x0800), дължина 1516: (tos 0x0, ttl 63, ид 53652, офсет 0, флагове [+], протокол IPIP (4), дължина 1500)
11.11.11.11 > 22.22.22.22: отрязана-ip - 20 байта липсват! (tos 0x0, ttl 50, ид 57750, офсет 0, флагове [DF], протокол TCP (6), дължина 1500)
33.33.33.33.33333 > 44.44.44.44.80: Флагове [.], seq 0:1448, ack 1, win 343, опции [nop,nop,TS val 11660691 ecr 2998165860], дължина 1448
0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
0x0010: 4500 05dc d194 2000 3f09 d5fb 0a66 387d E.......?....f8}
0x0020: 1x67 7899 4500 06xx e198 4000 3206 6xx4 .faEE.....@.2.m.
0x0030: b291 x9xx x345 2541 83b9 0050 9740 0x04 .......A...P.@..
0x0040: 6444 4939 8010 0257 8c3c 0000 0101 080x dDI9...W.......
0x0050: 00b1 ed93 b2b4 6964 xxd8 ffe1 006a 4578 ......ad.....jЕх
0x0060: 6966 0000 4x4d 002а 0500 0008 0004 0100 if..MM.*........
14:02:58.471103 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 63, id 53652, offset 1480, flags [none], proto IPIP (4), length 40)
11.11.11.11 > 22.22.22.22: ip-proto-4
0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
0x0010: 4500 0028 d194 00b9 3f04 faf6 2x76 385x E..(....?....f8}
0x0020: 1x76 6545 xxxx 1x11 2d2c 0c21 8016 8e43 .faE...D-,.!...C
0x0030: x978 e91d x9b0 d608 0000 0000 0000 7c31 .x............|Q
0x0040: 881d c4b6 0000 0000 0000 0000 0000 ..............
Това са два фрагмента на един пакет (с еднакъв ID 53652) с фотография (ясно се вижда думата Exif в първия пакет). Поради факта, че на това ниво пакети има, а в обединен вид в дампите — няма, проблемът явно е със сглобяването. Най-накрая има документално потвърждение на това!
Декодерът на пакети не откри проблеми, пречещи на сглобяването. Пробвах тук: . Първо, при опит да се вмъкне нещо в него, декодерът не одобри формата на пакета. Оказа се, че имаше излишни два октета между Srcmac и Ethertype (непредставляващи информация за фрагменти). След тяхното премахване декодерът започна да работи. Въпреки това не показваше никакви проблеми.
Каквото и да правим, освен онези Sysctl, не се намери нищо друго. Оставаше да се намери начин за идентифициране на проблемните сървъри, за да се разбере мащаба и да се вземе решение за следващи стъпки. Достатъчно бързо се намери необходимият брояч:
netstat -s | grep "packet reassembles failed”
Той също така присъства в snmpd под OID=1.3.6.1.2.1.4.31.1.1.16.1 ().
«Брой на неуспехите, открити от алгоритъма за повторно изграждане на IP (по каквато и да е причина: изтекло време, грешки и т.н.)».
Сред групата сървъри, на които се проучваше проблема, на два брояча нарастваше по-бързо, на два — по-бавно, а на още два изобщо не нарастваше. Сравнението на динамиката на този брояч с динамиката на HTTP-ошибките на Java-сървъра разкри корелация. Тоест, броячът можеше да бъде поставен на мониторинг.
Наличието на надежден индикатор за проблемите е изключително важно, за да може точно да се определи дали откатът на Sysctl помага, тъй като от предишния разказ знаем, че по приложението това не може да се разбере веднага. Този индикатор би позволил да се открият всички проблемни места в продукцията преди да бъдат забелязани от потребителите.
След отката на Sysctl грешките в мониторинга спряха, така че причината за проблемите беше доказана, както и фактът, че откатът помага.
Ние откатихме настройките за фрагментация на други сървъри, където се активира нов мониторинг, а на някои дори бяха отделени повече памет за фрагменти, отколкото беше по подразбиране преди това (ставаше въпрос за udp-статистика, частичната загуба от която не се забелязваше на фона на общата).
Най-важните въпроси
Защо на нашия L3-балансатор пакетите се фрагментират? Повечето пакети, които идват от потребителите към балансаторите, са SYN и ACK. Размерите на тези пакети са малки. Но тъй като делът на подобни пакети е много голям, не забелязахме наличието на големи пакети, които започнаха да се фрагментират.
Причината стана повреден скрипт за конфигурация на сървъри с Vlan интерфейси (по времето, когато сървъри с тагиран трафик в продукция имаше много малко). Advmss позволява да предадем на клиента информация, че пакетите в наша посока трябва да бъдат с по-малък размер, така че след прикрепянето на заглавията на тунела, те да не се налага да бъдат фрагментиране.
Защо откатът на Sysctl не помогна, а рестартирането помогна? Откатът на Sysctl променяше обема памет, достъпен за съединяване на пакети. В този случай, очевидно фактът, че паметта за фрагменти е пренаселена, е водила до забавяне на връзките, което е довело до дълго задържане на фрагментите в опашката. Тоест, процесът е бил цикличен.
Рестартирането нулираше паметта и всичко се подреждаше.
Можеше ли да се мине без Workaround? Да, но рискът от оставяне на потребителите без услуги в случай на атака е много голям. Разбира се, прилагането на Workaround в резултат доведе до възникването на различни проблеми, включително забавяне на един от услугите за потребителите, но все пак смятаме, че действията бяха оправдани.
Благодаря на Андрей Тимофеев () за помощта в провеждането на разследването, както и на Алексей Кренев () — за титаничния труд по обновяване на Centos и ядрото на сървърите. Процес, който в конкретния случай няколко пъти се наложи да започва отначало, поради което се проточи с много месеци.
Източник: habr.com
