OpenBSD : Оптимизация правил файрвола

Задача

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

Всё же, в реальности несколько факторов ограничивают то, насколько успешно пакетный фильтр справляется с возложенной на него задачей. Пакеты, проходящие через устройство, получают дополнительную задержку по времени в тот момент, когда попадают в устройство и когда они его покидают. Ведь любое устройство может обрабатывать некоторое конечное количество пакетов за секунду. Если же пакеты прибывают с большей скоростью – они теряются.

Большинство протоколов, таких как TCP, хорошо уживаются с задержками. Можно достигнуть высоких скоростей даже на линиях связи, в которых задержка передачи составляет несколько сотен миллисекунд. С другой стороны для интерактивных сетевых игр даже несколько десятков миллисекунд это слишком много. Здесь потеря данных становится критичной. Производительность TCP серьёзно снижается при большом количестве потерянных пакетов.

Эта статься объясняет, как определить, что пакетный фильтр стал тем узким местом в сети, которое ограничивает пропускную способность и что сделать, чтобы этого избежать.
Значение интенсивности поступления пакетов.

Общепринятая единица измерения пропускной способности сети это пропускная способность в байтах в секунду. Но она становится совершенно непригодна, когда речь идёт о пакетном фильтре. На самом деле, ограничивающим фактором здесь является не пропускная способность, в общепринятом смысле этого слова, а то, какое количество пакетов может обработать данный хост за секунду. К примеру, хост, без труда обрабатывающий полосу в 100Mbps с пакетами размером 1500 байт, может быть легко загружен полосой в 10Mbps состоящей из 40 байтных пакетов. Первая пара значений подразумевает только 8000 пакетов в секунду, в то время как вторая уже 32000, что означает увеличение нагрузки на хост приблизительно в 4 раза.

Чтобы уяснить это, давайте посмотрим, как в реальности пакеты проходят через хост. Принимаемые из сети данные сначала накапливаются в небольшом  внутреннем буфере сетевого адаптера. Когда он заполняется, сетевая карта генерирует прерывание, заставляющее её драйвер скопировать пакет(ы) в сетевой буфер ядра (т.н. mbufs). Пакеты передаются стеку TCP/IP в том виде, в каком они находятся в mbufs. Когда пакет попадает в буфер ядра, большинство операций, производимых с ним, не зависят от его размера, т.к. для них имеет значение только заголовки, а не некоторая общая нагрузка. Это также верно и для пакетного фильтра,  через который проходит пакет в единицу времени, и который принимает решение, заблокировать либо пропустить данный пакет. Если пакет следует перенаправить (forwarding), стек TCP/IP передаст его сетевой карте, которая, в свою очередь извлечет пакет из mbufs и передаст обратно в линию связи.

Большинство данных операций создаёт сравнительно большую нагрузку в расчёте на один пакет, и, в то же время достаточно низкую, если вести расчёт по размерам пакетов.

Некоторые из существующих ограничений основаны на программной и аппаратной части пакетного фильтра. Так, например машины класса i386, из-за особенностей своей архитектуры, не способны справиться больше чем с 10000 прерываниями в секунду, в независимости от того, насколько быстр процессор. Некоторые сетевые адаптеры генерируют прерывание на каждый принятый пакет. Значит хост начнёт терять эти пакеты уже при скорости около 10000 шт./секунду. Другие же, как большинство дорогих гигабитных адаптеров, имеют большую встроенную память под буферы, которая позволяет им выдать несколько пакетов за одно прерывание. Таким образом, выбор аппаратной части может налагать некоторые ограничения, которые не поможет преодолеть оптимизация самого пакетного фильтра.

Когда узкое место – пакетный фильтр.

Ядро передаёт пакеты для фильтра один за другим, по очереди. И в то время когда файрвол вершит судьбу какого-то пакета, поток остальных, проходящих через ядро, останавливается. В этот короткий промежуток времени, данным, считанным из сети сетевым адаптером, приходится размещаться в буферах. Если же расчёты затягиваются, пакеты быстро заполняют буферы и последующие приходящие пакеты просто теряются. Задача оптимизации правил пакетного фильтра состоит в том, чтобы сократить время, потраченное на обработку каждого пакета.

Вот одно интересное упражнение: умышленно переведём хост в вышеописанное состояние, загружая его большим количеством правил.
$ i=0; while [ $i -lt 100 ]; do \
    printf "block from any to %d.%d.%d.%d\n" \
    `jot -r -s " " 4 1 255`; \
    let i=i+1; \
done | pfctl -vf -

block drop inet from any to 151.153.227.25
block drop inet from any to 54.186.19.95
block drop inet from any to 165.143.57.178



Это пример наихудшего случая, который отвергает всю автоматическую оптимизацию, так как каждое правило содержит случайный не совпадающий адрес, и фильтр вынужден, проходя через набор правил, применить каждое правило к каждому пакету. Используя набор, который состоит из тысяч таких правил, а затем, генерируя непрерывный поток пакетов, который должен быть отфильтрован, можно создать заметную нагрузку даже на очень быстрой машине. Во время загрузки хоста можно проверить частоту поступления прерываний командой:
$ vmstat –i


и состояние процессора:
$ top


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

Теперь еще немного «экстрима»:
$ pfctl –d


А теперь сравните показания vmstat и top.

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

Если у вас уже есть работающий набор правил и вы недоумеваете, зачем его нужно оптимизировать по скорости, повторите этот тест с вашим набором и сравните результаты с приведёнными «экстремальными» случаями. Если в работе ваш набор правил вызывает признаки повышенной загрузки, можете смело использовать нижеприведённое руководство чтобы уменьшить этот эффект.

В некоторых случаях набор правил может не оказывать заметного влияния на систему и всё будет работать так как и ожидалось, до момента, когда возникнут непредвиденные проблемы, как, например задержки при установлении соединения, «подвисания» соединений, или подозрительно низкая пропускная способность. В большинстве случаев проблема оказывается вовсе не в производительности системы фильтрации, а в неправильной конфигурации набора правил, которая приводит к потере пакетов. Глава «Проверка вашего файрвола» покажет, как обнаружить и устранить подобные проблемы.

В конечном счёте, если ваш набор правил обрабатывается не оказывая заметного влияния на производительность, и всё работает так, как и следовало, наилучшим решением в данном случае будет решение «Оставить всё как есть». Часто правила написанные с использованием прямолинейного подхода без оглядки на производительность обрабатываются достаточно быстро, не приводя к потере пакетов, а дальнейшая их ручная оптимизация приводит к ухудшению читабельности, при этом, не сильно улучшая производительность.

Фильтрация с отслеживанием состояния соединения

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

Для каждого пакета сначала производится просмотр таблицы состояний. Если найдено совпадение, пакет немедленно пропускается фильтром. В противном случае пакетный фильтр начинает обрабатывать список правил фильтрации, находит последнее подходящее к данному пакету правило, и, если согласно этому правилу пакет нужно пропустить, создаёт запись в таблице состояний если для правила включена опция ‘keep state’.

Если же фильтрация проводится без использования таблицы состояний, то каждый пакет вызывает просмотр всего списка правил, а именно эта часть и является наиболее ресурсоёмкой. Запрос к таблице всё же производится, но так как она пуста, то это эффективность такого запроса равна нулю.

Фильтрация с отслеживанием состояния соединения подразумевает собой использование флага «keep state» в правилах файрвола, таким образом, пакеты, подходящие под эти правила создадут новую запись в таблице состояния соединений. Последующие пакеты, относящиеся к тем же соединениям, подойдут под запись в таблице и будут пропущены автоматически, без проведения процедуры проверки правилами пакетного фильтра. При этом развитии сценария, только первый пакет в каждом соединении вызовет обсчёт его по списку правил, последующие же – только выборку из таблицы состояний.

Таким образом, в плане производительности, выборка из таблицы обходится значительно дешевле, нежели вычисление набора правил фильтра. Но эта цена возрастает с каждым новым правилом в списке: в два раза больше список правил – в два раза возрастёт объём работы. Даже проверка одним правилом вызывает сравнение многочисленных полей пакета. Таблицу же состояний можно представить в виде дерева: затраты на выборку возрастают в логарифмической прогрессии от количества записей в таблице, т.е. в два раза больший объём таблицы вызовет увеличение затрат на одну условную единицу. К тому же, для проверки по таблице необходимы только несколько значений  полей пакета.

Само по себе создание и удаление записей в таблице состояний создаёт некоторую нагрузку. Но, предполагая что под созданную запись подойдёт несколько пакетов в рамках соединения и это упростит процедуру прохождения фильтрации для них, суммарные затраты все же намного меньше. Для некоторых типов соединений, таких как DNS запросы, когда каждое соединение состоит из двух пакетов (запрос и ответ), преимущество создания записи в таблице состояния сводится на нет, по сравнению с двумя просмотрами списка правил. Соединения, которые состоят из большего числа пакетов, как и большинство TCP соединений, только выиграют от создания записи в таблице состояний.

Вкратце, можно ассоциировать вычисление правил файрвола не с пакетом, а с соединением. Это даст коэффициент порядка 100 или более. Например, наблюдая за счётчиками:
$ pfctl –si

State Table        Total        Rate
   Searches        172507978    887.4/s
   inserts            1099936        5.7/s
   removals        1099897        5.7/s
Counters
   Match            6786911        34.9/s


Видно, что к пакетному фильтру происходят обращения примерно 900 раз в секунду. Я использую фильтрацию на нескольких интерфейсах, это значит, перенаправляется около 450 пакетов в секунду, каждый из которых  проверяется дважды, по разу на каждом интерфейсе, через который проходит. Но просмотр правил выполняется только 35 раз в секунду, а новые записи в таблицу состояний заносятся  и удаляются вообще 6 раз в секунду. С чем-чем, а с маленьких набором правил это становится достаточно важным.

Чтобы убедится в том, что в самом деле создаются записи для каждого соединения, поищите записи ‘pass’ в правилах, которые не используют опцию ‘keep state’:
$ pfctl -sr | grep pass | grep -v 'keep state'


Удостоверьтесь в том, что политика по умолчанию выставлена в ‘block by default’, так как в противном случае, пакеты будут пропускаться не из-за их попадания под правила ‘pass’, а из-за несоответствия ни одному правилу и попадания под политику по-умолчанию.

Оборотная сторона фильтрации с отслеживанием состояния соединения.

Единственным подводным камнем этой схемы фильтрации является то, что под каждое поле таблицы выделяется некоторый объём памяти, примерно 256 байт на каждую запись. Когда пакетный фильтр не может выделить память под новую запись, он блокирует пакет, который должен был быть внесён в таблицу и увеличвает счётчик “out of memory”. Его значение  можно посмотреть командой:
$ pfctl -si
 Counters
   memory                                 0            0.0/s


Память выделяемая под записи в таблице состояний заимствуется из пула ‘pfstatepl’. Можно использовать vmstat, чтобы увидеть различные аспекты использования этого пула:
$ vmstat -m
 Memory resource pool statistics
 Name        Size Requests Fail Releases Pgreq Pgrel Npage Hiwat Minpg Maxpg Idle
 pfstatepl    256  1105099    0  1105062   183   114    69   127     0 625   62


Разница между ‘Requests’ и ‘Releases’ равна количеству занятых записей в таблице состояний соединений, которая должна совпадать со значением счётчика, отображаемого командой:
$ pfctl -si
State Table                          Total             Rate
current entries                       36


Другие счётчики, выводимые через pfctl могут быть сброшены через pfctl –Fi

Не вся память установленная в системе доступна для ядра, её количество определяется архитектурой, опциями ядра, а также его версией. В OpenBSD 3.6 и ядром под i386 доступно к использованию 256Mb. Вы можете иметь 8Gb ОЗУ, а пакетный фильтр будет сообщать о невозможности выделения памяти.

Чтобы ещё больше сгустить краски, отметим, что когда пакетный фильтр доходит до состояния, когда функция ‘pool_get(9)’ возвращает ошибку, общее состояние будет ухудшаться не плавно, как бы нам этого не хотелось. Напротив, вся система становится нестабильной и, в конечном счёте, рушится. На самом деле это вина не пакетного фильтра, а общая проблема управления пулом памяти ядра.

Чтобы корректно обрабатывать подобные ситуации пакетный фильтр ограничивает количество одновременно существующих записей в таблице состояний соединений, используя функцию pool_sethardlimit(9). Количество записей можно посмотреть командой vmatat –m. Значение по умолчанию это 10000 записей, оно должно подойти любому стандартному хосту. Предельные значения выводятся на экран командой pfctl –sm:
$ pfctl -sm
 states     hard limit  10000
 src-nodes  hard limit  10000
 frags      hard limit    500


Если вам необходимы более высокие значения, можете увеличить их изменив файл pf.conf:
set limit states 10000


Существует проблема выбора величины этого значения, так, чтобы не возникало ошибок при выделении памяти. Это всё еще актуальная тема, поэтому не существует простой формулы для того чтобы вычислить значение предела. В простейшем случае, вы можете увеличивать значение, при этом проверяя стабильность работы хоста, искусственно создавая большое количество записей.

При хорошем раскладе, если у вас есть 512Mb ОЗУ, можно использовать до 256Mb для нужд ядра, что обеспечивает обработку до 500000 записей в таблице состояний. Многие полаг