Оптимизация правил брандмауэра во FreeBSD


Даниел Хартмеиер, перевод SecurityLab.ru.


Задача

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

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

Большинство протоколов, таких как 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 "
`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 записей в таблице состояний. Многие полагают, что это означает большое количество одновременных соединений. Однако представьте себе, что каждое из этих соединений генерирует один пакет каждую секунду, в таком случае получим 50000 пакетов/секунду!

Но наверняка вам не стоит ожидать таких значений. Каков бы ни был установленный вами предел количества состояний, могут быть случаи, когда он будет достигнут, к примеру во время DoS-атаки. Атакующий может создавать соединения до тех пор, пока не будет достигнуто установленное ограничение, с целью сделать невозможным доступ к сервису легитимных пользователей.

Есть несколько путей решения этой проблемы:

Вы можете ограничить количество записей в таблице только для некоторых правил, например вот так:
pass in from any to $ext_if port www keep state (max 256)

Это установит количество одновременных соединений к web-серверу в 256, в то время как по остальным правилам фильтрации записи в таблицу состояний будут возможны. Таким же образом можно лимитировать количество соединений с одного удалённого хоста:
pass keep state (source-track rule, max-src-states 16)

Как только появляется запись в таблице состояния соединений, несколько таймаутов задают, как скоро это соединение будет удалено из таблицы. Например:
$ pfctl -st
tcp.opening 30s

Таймаут для не полностью установленных соединений равен 30 секундам. Значения этих таймаутов могут быть понижены для более агрессивного удаления записей из таблицы состояний. Значения могут быть выставлены в файле pf.conf:
set timeout tcp.opening 20

Они также могут быть применены к отдельным правилам фильтра:
pass keep state (tcp.opening 10)
Существет несколько наборов предопределённых значений, которые могут быть выбраны в том же pf.conf:
set optimization aggressive

Также существуют адапивные таймауты, значения которых не являются константами, а меняются в звисимости от количества записей в таблице состояний. Пример:
set timeout { adaptive.start 6000, adaptive.end 12000 }

Пакетный фильтр будет использовать постоянные значения таймаутов до тех пор, пока количество записей в таблице на достигнет 6000. В то время когда это количество между 6000 и 12000, значения всех таймаутов будут линейно меняться от 100%, при 6000 соединениях в таблице, до 0%, при 12000 записей в таблице, то есть соответственно при 9000 все значения таймаутов будут уменьшены на 50%.

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

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

Следовательно, в первом приближении, издержки производительности при обработке набора правил возрастают с одновременно с числом правил. Это не является абсолютно точной зависимостью по причинам, с которыми нам вскоре предстоит столкнуться, однако основная концепция такова. Набор из 10000 правил почти наверняка вызовет намного большую загрузку вашего хоста, чем набор из 100. Значит первоочередная оптимизация – сокращение списка правил.

Упорядочивание списка для увеличения шага пропуска.

Первая причина, по которой обработка списка правил пакетным фильтром вызывает меньшие затраты чем просто последовательная обработка каждого правила списка, это т.н. «шаг пропуска» («skip steps»). Это прозрачная для пользователя и автоматическая оптимизация, выполняемая пакетным фильтром. Лучше объяснить её смысл на примере. Допустим у нас есть простой набор правил:
1. block in all
2. pass in on fxp0 proto tcp from any to 10.1.2.3 port 22 keep state
3. pass in on fxp0 proto tcp from any to 10.1.2.3 port 25 keep state
4. pass in on fxp0 proto tcp from any to 10.1.2.3 port 80 keep state
5. pass in on fxp0 proto tcp from any to 10.2.3.4 port 80 keep state

Пакет TCP приходит на интерфейс fxp0 с адресом назначения 10.2.3.4 и с каким-нибудь портом назначения, значение которого не важно. Пакетный фильтр начинает обработку набора правил с первого правила, под которое кстати этот пакет подходит. Обработка продолжается на втором правиле, для которого выполняются критерии ‘in’,’on fxp0’,’proto tcp’,’from any’, но не совпадает поле адреса назначения ’10.1.2.3’. Так как пакет не подходит под второе правило, обработка продолжается на третьем правиле.

Но пакетный фильтр «знает», что третье и четвёртое правило требуют проверки того же критерия – ‘to 10.1.2.3’, который вызвал невыполнение условия совпадения во втором правиле. Можно абсолютно точно сказать, что третье и четвёртое правила не подойдут к данному пакету, и осуществляется немедленный переход к пятому правилу, экономя таким образом некоторые вычислительные ресурсы.

А теперь представьте, что пакет, попавший под проверку был протокола UDP, а не TCP. Пакет подойдёт под первое правило, и попадёт на обработку со вторым. Здесь указан критерий проверки ‘proto tcp’, под который проверяемый пакет на подходит. А так как последующие правила также требуют проверки по этому же критерию, который вызвал несовпадение, все они могут быть пропущены, не требуя в результате дополнительных вычислений.

Вот так происходит анализ вашего списка правил пакетным фильтром. Каждое правило может содержать список критериев, таких как ‘to 10.1.2.3’, заставляя правило действовать только на пакеты с этим адресом назначения. Для каждого критерия в правиле фильтр подсчитывает количество правил, следующих непосредственно за обрабатываемым, которые используют те же критерии, что и текущее обрабатываемое. Вычисленное значение может быть как нулём (если ни одно последующее правило не использует те же критерии), так и числом вплоть до количества оставшихся правил и сохраняется в памяти для последующего использования. Значение называется «шаг пропуска», так как содержит количество правил, которые необходимо пропустить если любое из полей пакета не удовлетворяет критерию правила.

Обработка полей правила происходит в заданном порядке:
Интерфейс ('on fxp0')
Направление ('in', 'out')
тип адреса ('inet' or 'inet6')
протокол ('proto tcp')
адрес источника ('from 10.1.2.3')
порт источник ('from port < 1024')
адрес назначения ('to 10.2.3.4')
порт назначения ('to port 80')

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

Очевидно, порядок обработки правил в вашем наборе будет влиять на значение шага пропуска вычисляемого для каждого правила.

Например:
pass on fxp0
pass on fxp1
pass on fxp0
pass on fxp1

Этот набор правил приведёт к использованию шага пропуска равного нулю, потому что смежные правила не содержат одного и того же критерия выбора по интерфейсу. Эти правила должны располагаться так:
1. pass on fxp0
2. pass on fxp0
3. pass on fxp1
4. pass on fxp1

Значение шага пропуска будет равно единице для первого и третьего правила. Это выявляет небольшое отличие при обработке этого набора для интерфейса fxp2. Перед проведённой перестановкой были бы проверены все правила, а после перестановки только правила 1 и 3, соответственно 2ое и 4ое пропущены.

Различие может показаться несущественным в этом маленьком примере, но представьте набор правил, состоящий из 1000 записей, которые будут применяться только к двум различным интерфейсам. Если вы расположите эти правила, так, что записи, относящиеся к одному интерфейсу будут сгруппированы, пакетный фильтр может просто пропускать остальные 500 правил при обработке пакета, снижая таким образом издержки производительности на обработку списка правил на 50%, в независимости от того, из каких пакетов состоит ваш трафик.

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

Чтобы увидеть эффект от ваших действий запустите

$ pfctl –gsr

pfctl выведет рассчитанное значение шага пропуска по каждому критерию, например:
@18 block return-rst in quick on kue0 proto tcp from any to any port = 1433

[ Skip steps: i=38 d=38 f=41 p=27 sa=48 sp=end da=43 ]

В этом выводе команды ‘i’ обозначает интерфейс, ‘d’ - направление, ‘f’ - тип адреса, итд.

‘i=38’ значит, что пакет не подходящий под проверку интерфейса на совпадение с ‘kue0’, вызовет пропуск поледующих 38 правил.

Это также влияет на счётчик проведённых для правила сравнений, ведущийся для каждого правила.

$ pfctl –vsr

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

Использование списков, заключённых в фигурные скобки, позволяет писать очень компактные правила, к примеру:
pass proto tcp to { 10.1.2.3, 10.2.3.4 } port { ssh, www }

Но в реальности, при загрузке этих правил, они не выглядят как одно правило. Напротив, pfctl разворачивает одно правило в несколько, в нашем случае это будет:
$ echo "pass proto tcp to { 10.1.2.3, 10.2.3.4 } port { ssh, www }" |
pfctl -nvf -
pass inet proto tcp from any to 10.1.2.3 port = ssh keep state
pass inet proto tcp from any to 10.1.2.3 port = www keep state
pass inet proto tcp from any to 10.2.3.4 port = ssh keep state
pass inet proto tcp from any to 10.2.3.4 port = www keep state

Краткий синтаксис в pf.conf скрывает реальную ситуацию, возникающую при обработке этих правил. Ваш pf.conf может быть всего в десяток сточек, но для ядра они развернутся в сотни, и ёмкость вычислений будет такой же, как если бы вы поместили эти сотни правил непосредственно в pf.conf. Чтобы увидеть, какие же правила обрабатываются в реальности, введите:
$ pfctl –sr
Для одного из типов списков – списков адресов, существует специальное хранилище в ядре, называемое таблицей.
pass in from { 10.1.2.3, 10.2.3.4, 10.3.4.5 }
Список адресов может быть представлен в виде именованной таблицы:
table <clients> const { 10.1.2.3, 10.2.3.4, 10.3.4.5 }
pass in from <clients>
Эта конструкция может быть загружена в ядро одним правилом, в то время использование других представлений списков будет развёрнуто в три правила.

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

Если список адресов большой, преимущество этого метода по производительности перед вычислением адресов в отдельных правилах, довольно значительно. Как правило, использование таблиц становится выгодным при использовании шести и более адресов в списке. Для списка же в 1000 адресов, разница составит 1000.
Использование опции quick для отмены дальнейшего просмотра правил при первом совпадении

При первом совпадении правила для текущего обрабатываемого пакета, пакетный фильтр (в отличие от некоторых других программных продуктов, предназначенных для фильтрации трафика) не прекращает обработку набора правил, до тех пор, пока не будет обработан весь набор. Если же достигнут конец списка правил, решение принимается по последнему совпавшему правилу (принцип «последнего совпадения»). Опция 'quick' используется для прекращения просмотра списка после первого совпадения. Если опция используется для каждого правила, пакетный фильтр начинает работать по принципу «первого совпадения».

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

set skip on { lo0 }

Список может содержать сотни правил, которые не подходят под интерфейс loopback, и трафик, проходящий через него просто попадёт под правило по умолчанию 'pass'. Разница в вычислениях будет равна длине списка правил в расчёте на каждый пакет, проходящий через loopback.

Обычно, вы бы поставили опцию 'quick' в начало списка правил, аргументируя тем, что при выполнении этого правила, сократятся последующие вычисления. Но в тех случаях, когда правило не будет выполняться, подобная политика вызовет дополнительные вычисления. Если вкратце, то частота (видимо здесь подразумевается «математическое ожидание» - прим. переводчика), с которой ожидается появление совпадений для правила, должна приниматься в расчёт при выборе местоположения правила в списке, а эта частота зависит от вашего трафика.

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

$ pfctl –vsr

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

Вложения это, по сути дела, отдельные наборы правил, вы можете загружать множество правил во вложения и заставить их выполняться из основного списка.

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

В OpenBSD 3.6 вы также можете вызывать одно вложение из другого, строя иерархию процедур, вызываемых одна из другой. В версиях OpenBSD от 3.5 и младше, иерархия может быть глубиной только в 1 уровень, то есть у вас может быть несколько процедур, но вызываться они могут только из основного набора правил.

Пример:
pass in proto tcp from 10.1.2.3 to 10.2.3.4 port www
pass in proto udp from 10.1.2.3 to 10.2.3.4
pass in proto tcp from 10.1.2.4 to 10.2.3.5 port www
pass in proto tcp from 10.1.2.4 to 10.2.3.5 port ssh
pass in proto udp from 10.1.2.4 to 10.2.3.5
pass in proto tcp from 10.1.2.5 to 10.2.3.6 port www
pass in proto udp from 10.1.2.5 to 10.2.3.6
pass in proto tcp from 10.1.2.6 to 10.2.3.7 port www

Можно разделить этот список на два подсписка, один из которых называется «udp-only»:

pass in proto udp from 10.1.2.3 to 10.2.3.4
pass in proto udp from 10.1.2.3 to 10.2.3.4
pass in proto udp from 10.1.2.4 to 10.2.3.5
pass in proto udp from 10.1.2.5 to 10.2.3.6

А второй «tcp-only»:
pass in proto tcp from 10.1.2.3 to 10.2.3.4 port www
pass in proto tcp from 10.1.2.4 to 10.2.3.5 port www
pass in proto tcp from 10.1.2.4 to 10.2.3.5 port ssh
pass in proto tcp from 10.1.2.5 to 10.2.3.6 port www
pass in proto tcp from 10.1.2.6 to 10.2.3.7 port www

Оба мугут быть вызваны из основного списка правил командами:
anchor udp-only
anchor tcp-only
Впрочем, использование вложений не намного улучшит производительность. Даже наоборот, возникают некоторые дополнительные издержки, когда ядро вынуждено обращаться к дополнительным вложенным спискам. Однако вызовы вложений могут содержать условия, почти также как и правила pass/block:

anchor udp-only in on fxp0 inet proto udp

anchor tcp-only in on fxp0 inet proto tcp,

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

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

Начиная с OpenBSD 3.6, некоторые виды оптимизации, которые здесь рассматриваются, могут быть автоматически проведены с помощью pfctl -o. В процедуре оптимизации анализируется набор правил и проводятся модификации, которые не влияют на результат выполнения.

Во первых, pfctl разбивает список на блоки смежных правил так, что перекомпоновка в пределах блока ни каким образом не влияет на обработку пакета.

Например, правила в следующем блоки могут быть перераспределены произвольно:
pass proto tcp to 10.1.2.3 port www keep state
pass proto udp to 10.1.2.3 port domain keep state
pass proto tcp to 10.1.0.0/16 keep state

Но, в большинстве случаев, порядок правил имеет значение. Пример:
block log all
block from 10.1.2.3
pass from any to 10.2.3.4
Изменение положения любого из этих правил приводит к совершенно разным результатам. После перестановки первых двух правил, пакеты от 10.1.2.3 также блокируются, но теперь уже и отмечаются в лог-файле. Перестановка последних двух правил ведёт к тому, что пакеты от 10.1.2.3 к 10.2.3.4 будут заблокированы. А перестановка первого и последнего правила повлечет за собой блокирование всех пакетов. В каждом случае, при наличии взаимной зависимости, pfctl делит правила на блоки. В наихудшем случае, когда невозможна перестановка двух соседних правил, каждое правило рассматривается как отдельный блок, содержащий одно правило и pfctl не может в данном случае сделать никаких изменений.

В противном случает, pfct сортирует правила в каждом блоке, чтобы увеличить шаг пропуска:
$ cat example
pass proto tcp from 10.0.0.3 to 10.0.0.8
pass proto udp from 10.0.0.1
pass proto tcp from 10.0.0.2
pass proto tcp from 10.0.0.4
pass proto udp from 10.0.0.6
pass proto tcp from 10.0.0.3 to 10.0.0.7

$ pfctl -onvf example
pass inet proto tcp from 10.0.0.3 to 10.0.0.8
pass inet proto tcp from 10.0.0.3 to 10.0.0.7
pass inet proto tcp from 10.0.0.2 to any
pass inet proto tcp from 10.0.0.4 to any
pass inet proto udp from 10.0.0.1 to any
pass inet proto udp from 10.0.0.6 to any

Если обнаруживаются дублирующие правила, то они удаляются:
$ cat example
pass proto tcp from 10.0.0.1
pass proto udp from 10.0.0.2
pass proto tcp from 10.0.0.1

$ pfctl -onvf example
pass inet proto tcp from 10.0.0.1 to any
pass inet proto udp from 10.0.0.2 to any

Избыточне правила также удаляются:
$ cat example
pass proto tcp from 10.1/16
pass proto tcp from 10.1.2.3
pass proto tcp from 10/8

$ pfctl -onvf example
pass inet proto tcp from 10.0.0.0/8 to any

Где это возможно, несколько совмещаются в одно используя вложения:
$ cat example
pass from 10.1.2.3
pass from 10.2.3.4
pass from 10.3.4.5
pass from 10.4.5.6
pass from 10.5.6.7
pass from 10.8.9.1

$ pfctl -onvf example
table <__automatic_0> const { 10.1.2.3 10.2.3.4 10.3.4.5 10.4.5.6
10.5.6.7 10.8.9.1 }
pass inet from <__automatic_0> to any
Если pfctl вызывается с опцией -oo, будут также задействованы счётчики, выдаваемые по команде pfctl -vsr, чтобы переупорядочить правила содержащие 'quick' в соответствии с частотой их употребления.

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

Наилучший способ проверить, насколько полезны оказались опции -o или -oo для вашего набора правил, это сравнить его с изменённым:
$ pfctl -nvf /etc/pf.conf >before
$ pfctl -oonvf /etc/pf.conf >after
$ diff -u before after
Если сравнивать с набором правил, оптимизированным вручную, то различия обычно почти незаметны. Значительные же отличия могут быть отмечены для тех наборов правил, которые сгенерированы фронтэндами для редактирования правил файрвола.

Обновлено: 12.03.2015