Saturday, November 26, 2016

Adventures with FortiGate LB

Столкнулся на днях с довольно загадочной ситуацией.

Преамбула
Дано: есть сайт об осьми веб-серверах. Есть перед ними load balancer в виде файерволла FortiGate v5.4.1 - распределяет между ними нагрузку, а заодно переводит HTTPS-трафик в HTTP и инспектирует его. А перед ним находится сеть CDN, которая в свою очередь также шерстит трафик на предмет всяких там SQL-иньекций, блокирует DDoS-атаки, кэширует картинки и прочую статику - в общем, полезная вещь.

Когда юзер открывает сайт, DNS направляет его браузер на прокси CDN, браузер устанавливает с прокси SSL-соединение и посылает ему HTTP-запрос. Если запрос прокси нравится - пересылает его по другому SSL-соединению на LB. Тот прогоняет свои проверки - IPS, антивирус, - и перекидывает запрос на один из восьми серверов. Ответ сервера пересылается обратным путём.

Как видим, всё несложно, но есть дополнительное требование: sessions persistence, так же известное как sticky sessions. Сиречь, начав работать с каким-то сервером, юзер так и должен продолжать с ним работать - все запросы от браузера должны приходить на тот же сервер, разве что тот загнётся.
За это у нас отвечает FortiGate - он этого может добиваться одним из двух способов:

  • HTTP cookie - когда приходит запрос от нового клиента, FortiGate добавляет в ответ новый cookie с названием FGTServer и длинным псевдорандомальным значением. Браузер запоминает этот cookie и добавляет в каждый последующий запрос. По его значению Фортигейт знает, на какой из серверов эти запросы перекидывать.
  • SSL Session ID - когда клиент с сервером устанавливает SSL-соединение, сервер сочиняет некий session identifier и посылает его клиенту. Если TCP-connection, на котором SSL-соединение основано, сломалось, клиент может быстро восстановить общение с сервером, открыв новую TCP-трубу и выслав этот identifier - избежав таким образом необходимости в полном обмене ключами. Это ускоряет SSL и расцепляет его с TCP - одно SSL-соединение может использовать множество TCP-соединений. Подробности можно узнать здесь.
    В данном случае "сервер" - это не веб-сервер, а сам FortiGate. Он запоминает, на какой настоящий сервер он перебросил запрос, пришедший по соединению с новым session ID, и последующие запросы направляет соответственно.
Мы используем первый способ.

А теперь амбула
Выплыла проблема: юзерская сессия ломается. Вот залогинился он на сайт, и всё вроде отлично, но вдруг сайт ему и говорит: а ты кто такой, мил человек? Знать тебя не знаю.

Стало быть, не фурычит наша sessions persistence - попадают запросы от юзера вдруг на новый сервер, а не обслуживаются всё тем же. Открываем браузер, жмём F12, смотрим, что происходит - и впрямь: браузер получает в начале печеньку FGTServer, честно отсылает её - зато Фортигейт в какой-то момент от неё отмахивается, будто её и нету, перекидывает запрос на другой сервак и пришивает к ответу новое значение FGTServer.

Казалось бы, дело ясное - FortiGate виновен, на помойку. Но вот в чём закавыка: происходит это только при обращении через CDN - если браузер общается с Фортигейтом напрямую, то всё прекрасно, cookie при всех запросах сохраняется тот же, сервер - тот же, работает наш persistence!

Что за лабуда, думаю - может, CDN наши cookies срезает каким-то образом? Запускаю сниффер на Фортигейте, смотрю - ничего подобного! Вот он наш FGTServer в запросе, и значение правильное - да Фортигейт запрос на другой сервак перекидывает!

От отсутвия идей перешёл на второй способ, на SSL Session ID - не помогло ровно ничем. Напрямую - работает чудесно, через CDN - ломается. Что за притча?

Может, сервер health check проваливает и LB его помечает как дохлый? Может, мы максимум соединений на сервер перешли? Нет, вроде ничем не подтверждается - да и как объяснить тогда, что напрямую всё летает?


Развязка
А ларчик открылся вот как: оказывается, чёртов CDN запросы от множества разных клиентов мультиплексирует на уровне TCP. То есть приходят к ихнему прокси запросы от сотни юзеров - а тот поддерживает с нашим LB какой-то пул TCP-соединений - скажем, пять - и в каждое пихает запросы от разных юзеров.

Это performance, конечно, улучшает - не нужно для каждого нового юзера новый TCP-handshake устанавливать и отдельный сокет держать: бери да пихай его запросы в свободную TCP-трубу. Да только наш load balancer это с ума сводит - видать, не подумали программисты Fortinet, что такая ситуация возможна. При прямом-то обращении такого не бывает - по каждому TCP-коннекшену запросы только от одного клиента идут, с тем же самым FGTServer cookie - а тут он вдруг раз и меняется.

И уж понятно, почему SSL Session ID не помогает - соответствует-то он не клиенту, а CDN-прокси. Решил прокси пихнуть один запрос от клиента в один сокет, а следующий - в другой, и привет: session IDs разные, LB их отсылает на разные сервера.

Отключили мультиплексирование, заставили прокси на каждого клиента новую TCP-трубу открывать - и всё залетало. Вот такой вот хэппи энд, граждане.

P.S.
Была, впрочем, и альтернатива - почётную функцию load balancing на сам CDN возложить. Но его жадные хозяева хотели за это дополнительную денежку, а нас жаба задавила. Нам грубияны не нужны, мы сами грубияны.