IPFW: порядок прохождения пакетов, сложные случаи [2008]
Нарисовал тут давеча в RU.UNIX.BSD схему прохождения пакета через ядро и ipfw, с объяснениями, как это все стыкуется с divert, dummynet, keep-state и т.д., теория и примеры. Народу понравилось, решил опубликовать и здесь, чтоб не потерялось (ибо объяснений, как оно там все внутри, в сети не встречал - только howto-шки на что-то простое или конкретику "вот у меня наконец получилось", не дающие возможности понять и составить что-то сложное другое самому).
Newsgroups: fido7.ru.unix.bsd From: Vadim Goncharov <vadimnuclight@tpu.ru> Subject: ipfw: порядок прохождения пакетов, сложные случаи (was: ipfw fwd и natd) Date: Tue, 20 May 2008 13:48:53 +0000 (UTC) Message-ID: <1187235430@server.racoon.ru> Organization: Nuclear Lightning @ Tomsk, TPU AVTF Hostel X-Comment-To: Victor Sudakov X-FTN-Sender: Vadim Goncharov <vadim.goncharov@f400.n5020.z2.fidonet.org> X-FTN-REPLYTO: 2:5020/400 UUCP X-FTN-Tearline: slrn/0.9.8.1 on FreeBSD 6.2/i386
Hi Victor Sudakov!
On Mon, 12 May 2008 09:48:57 +0000 (UTC); Victor Sudakov wrote about 'Re: ipfw fwd и natd':
VS>>>>>>> Пакет, отправленный по ipfw fwd, уже больше через ipfw не проходит? VS>>>>>>> Как бы мне ухитриться сделать policy routing в некий интерфейс и VS>>>>>>> одновременно на этом интерфейсе поднять static NAT ? VS>>>>>> См. на тему ipfw one pass. Я сначала кладу пакет в divert, затем все VS>>>>>> что попало на следующее правило отправляю по fwd. VS>>>>> А я никак не могу грокнуть такие конфигурации ipfw, в которых правила VS>>>>> не привязаны к конкретным интерфейсам на in и out. Также как VS>>>>> конфигурации со skipto. VG>>>> А в чем конкретно затруднение? VS>>> Грокнуть не получается. VG>> Что в них конкретно вкурить не получается? VS> Пожалуй, порядок прохождения пакетов через правила и моменты, когда VS> пакет попадает на повторную обработку (divert etc).
Хм... ну попробую ниже.
VS> Hа иную конфигурацию смотришь, как на программу на бейсике с VS> бесконечными goto, пытаясь распутать клубок.
Да, оно и есть. Средство мощное, и потому пользоваться надо аккуратно... действительно, иные люди пишут очень запутанные конфиги, хотя можно было бы реорганизовать достаточно четко.
VG>> А то так в общем объяснять - скорее VG>> всего снова не вкурится. VS> Объяснять на пальцах бесполезно - на уровне мана оно и так понятно. VS> А вот именно интуитивно понять, почувствовать красоту этого дела и с VS> удовольствием начать пользоваться новым знанием - не выходит. VS> Просветление надобно.
Нууу.. на такие вещи надо видео показывать. Или при живом общении за кружкой пива в реальном времени на бумажке чертить. В письме это сложно сделать, но я попробую донести рисунками и аналогиями :)
Поскольку ipfw - довольно низкоуровневая штука, надобно объяснять с привлечением сведений для программиста, о вызовах конкретных функций. Пусть у нас ядро (и часть юзерленда) будет некоей местностью (аналогия на географию), по которой перемещается пакет. Пешком ли, поезд на сортировочной станции - неважно... Когда пакет прилетает в какую-нибудь функцию - он попадает в определенное место. Потом он по ней двигается (у нее есть протяженность) и уходит в какую-нибудь другую.
Это классическая картинка, тут вся машина в целом, видны оба прохода по ipfw. Допустим, набор правил у нас такой:
ipfw add 1 deny tcp from any to any 135,445 ipfw add 2 divert 8668 all from any to any via ext0 ipfw add 3 count icmp from any to any ipfw add 4 allow all from any to any # в 65535 по умолчанию deny
Цифрами на рисунке обозначены правила, через которые оно и проходит, в соответствующем порядке.
Когда пакет проходит через машину, к нему системой прикрепляется дополнительная информация, помимо собственно его содержимого, видного в tcpdump. Например, на каком интерфейсе он был получен, через какой отправляется, и т.п. Их можно проверять соответствующими опциями в правилах ipfw, на рисунке показаны места, где будут срабатывать соответствующие указания in/out и recv/xmit для сетевух.
Рассмотрим одну сторону роутера с рисунка выше более подробно, с точки зрения вызовов функций в ядре (чуть более подробная картинка из мана). На самом деле, с точки зрения функций, на рисунке выше нет двух сторон, она только одна, и различается параметром - обрабатываемой сетевухой.
То есть, пакет на входе передается драйвером в ether_demux(), затем он попадает в ip_input(), в точку (2), где выполняются базовые проверки на корректность пакета, после чего пакет прилетает в ipfw - функция ipfw_chk(). Допустим, правила были простые, без задействования других подсистем. Тогда, вернувшись из ipfw, пакет продолжает движение по ip_input(), которая смотрит, предназначен ли пакет нашей машине ("to me" в терминах ipfw), либо кому-то другому. Если нам, то пакет уходит в точку (3), где решится, в какой сокет какой юзерлэндной программе его отправить.
Если же пакет был предназначен не нам, пакет из ip_input() направится в точку (4), где ip_forward() проверит, установлен ли sysctl, разрешающий форвардинг, произведет декремент TTL и т.п. действия, после чего пакет придет в точку (6), функцию ip_output(). Туда же он попадет напрямую, когда какая-нибудь программа решит что-то отправить в сеть и передаст данные ядру.
Функция ip_output() первым делом смотрит в таблицу маршрутизации, определяя, каков шлюз и на каком интерфейсе он находится. С этой информацией пакет вновь передается в ipfw, в котором опять пробегается по всем правилам. После выхода из ipfw_chk() в ip_output(), если ядро было скомпилировано с соответствующей опцией, проверяется, не был ли применен ipfw fwd - если да, то просмотр таблицы маршрутизации выполняется заново с целью получить MAC-адрес нового шлюза. Затем пакет в точке (7) покидает ip_output() и передается дальше, на L2 и потом к драйверам интерфейсов.
Это всё было для случая простых правил файрвола. Теперь, предположим, там появляется divert, рассмотрим на примере правил выше. Пакет из внутренней сети куда-то в Интернет на порт 80 войдет на внутреннем интерфейсе в точку (1), пройдет начальные проверки ip_input() в (2), будет передан в ipfw_chk() и начнет проходить по правилам. Под правило 1 он не подпадает, под 2 тоже, так как имя интерфеса сейчас int0, правило 3 опять-таки не срабатывает, но под правило 4 подходят все пакеты, и он выходит из ipfw_chk() дальше в ip_input(). Там выясняется, что предназначен он идти в Интернет, поэтому пакет попадает в точки (4) и затем (6), где ip_output() определяет адрес шлюза и то, что интерфейс будет ext0, с чем пакет и попадает опять в ipfw_chk() и снова идет по правилам. Правило 1 снова не подходит, но условие правила 2 срабатывает - "via ext0, проходим прямо сейчас через интерфейс ext0, в любом направлении".
И вот здесь срабатывает divert - пакет из ipfw_chk() передается в точку (8), в подсистему divert, при этом к нему предварительно прикрепляется метаинформация с направлением (out), интерфейсом (ext0) и номером правила 2. Подсистема divert передает этот пакет в указанный в правиле порт (8668), на котором в нашем случае работает natd. Тот обрабатывает пакет, метаинформацию же - не трогает, и возвращает подсистеме divert вместе с измененным пакетом как есть (так поступают большинство divert-демонов, хотя любой их них, в принципе, может поменять эту информацию, и пакет будет передан в другое место).
Подсистема divert выводит полученный из natd пакет из точки (9) в точку (6). Следует обратить внимание - пакет попадает в ip_output() ЕЩЕ РАЗ! Это необходимо, так как демон мог вернуть пакет с совершенно другими адресами или вообще создать новый пакет. Но в нашем случае пакет несет с собой диверт-тег, метаинформацию, самая важная часть которой - номер правила. При входе в ipfw_chk() пакет первым делом проверяется на наличие этого самого тега. Он найден, и в нем содержится номер правила - 2. Поэтому ipfw_chk() пропускает правила номер 1 и 2, и начинает с номера 2+1, то есть 3 (если бы пакет применялся к указанному номеру, а не следующему, то он снова попал бы в divert, то есть получился бы бесконечный цикл).
На этом месте пакет продолжит движение с правила номер 3 и дальше, как обычно, уйдет в точке (7) в сеть. Таким образом, несмотря на то, что пакет попадал в ip_output() два раза, с точки зрения пользователя это выглядит так, как если бы он был там один раз и никуда из файрвола не убегал - просто на правиле 2 в нем волшебным образом поменялись адреса и порты.
Аналогичным образом, ответный пакет, возвращающийся из Интернета на интерфейс ext0, пройдет через машину по пути (1) - (2) - начало ipfw_chk() - правило 1 - правило 2 - (8) - divert - natd - divert - (9) - (2) - проверка в ipfw_chk() - правило 3 - правило 4 - (4) - (6) - начало ipfw_chk() - правило 1 - правило 2 - правило 3 - правило 4 - (7) - отправка через интерфейс int0.
Подобное выведение пакета из обработки ipfw в другую подсистему - не уникально для divert, это общая схема работы в стеке FreeBSD. Например, действия pipe и queue в dummynet, передача пакета в netgraph (а также появившийся в 7.0 ipfw nat) действуют по тому же принципу. Отличие, однако, в том, что в этих подсистемах пакет остается внутри ядра, никакому демону не передается. Поэтому, во-первых, подсистемы вместо номера правила сохраняют на него полный указатель, и пакет вернется непосредственно в следующее правило, даже если оно имеет тот же номер. Во-вторых, для таких подсистем действует настройка one_pass в соответствующем sysctl - если она включена, то при повторном входе пакета в ipfw после возврата из подсистемы dummynet (netgraph), ipfw_chk() сразу вернется без прохода по правилам, как если бы к пакету был применен allow. Это поведение позволяет упростить правила файрвола, когда известно, что если пакет попал в трубу, то он уже точно отправляется дальше, и не требуется после каждого pipe вставлять allow (чтобы пакет не попал в следующие правила и следующие pipe/queue). Если же конфигурация требует сначала ограничить трафик, а потом уже разбираться по замысловатым требованиям, что из него разрешить, а что запретить, то упрощению правил наоборот будет способствовать отключенный one_pass - поскольку с ним вместо allow, расположенных до pipe, пришлось бы делать skipto.
Итак, как уже было сказано, пакет проходит по списку правил последовательно, в порядке возрастания номеров правил. Список правил можно рассматривать как таблицу с тремя столбцами: номер, действие (и его параметры, например log), и условия, при которых пакет соответствует правилу (например, от адреса 1.1.1.1 адресу 2.2.2.2). Таблица просматривается сверху вниз, пакет сравнивается с условиями. На первом совпавшем условии смотрим в столбец действий, выполняем действие, прекращаем просмотр.
Это же можно, возвращаясь к географическим аналогиям, представить как беговую дорожку или коридор с инструкциями, типа "Если ваш вес больше 50 кг, поверните направо, иначе следуйте дальше". Соответственно, на первой же инструкции, которая подойдет, пакет свернет с прямой в нужную дверь. Можно заметить, что инструкции могут быть и вида "если вы болели в детстве ветрянкой, идите дальше и читайте следующую инструкцию на двери номер 150, а все, что встретится до нее - пропустите не читая". Это, очевидно, полный аналог действия skipto в ipfw.
Таким образом, работу ipfw_chk() можно упрощенно предствить в виде следующей блок-схемы:
Из схемы, пояснений и мана уже должно быть понятно, как это всё работает, и что skipto полностью аналогичен goto, и как он выглядел бы на схеме. Может возникнуть вопрос, зачем нужен skipto, если он нередко запутывает правила? Введен он, как это ни странно может показаться, как раз для возможности упрощения правил и увеличения производительности файрвола (а также позволяет делать интересные трюки с динамическими правилами, но об этом ниже). Для того, чтобы это понять, надо рассмотреть, как устроена часть правила ipfw, отвечающая за проверку соответствия пакета условию.
Ман говорит, что синтаксис "тела" правила (rule body) в ipfw2 есть [ протокол from набор_адресов1 to набор_адресов2 ] [опция1 [опция2 ...]] То есть, привычная часть "tcp from any to me" вообще говоря, необязательна, а в списке опций допустимы OR-блоки, то есть он, по сути своей, представляет то, что в математике называется конъюнктивной нормальной формой (КНФ) булева выражения. А сами опции - это предикаты, они могут быть истинны или ложны. Список опций в мане - и есть список таких предикатов для пакета. Все, что можно сделать в привычной части в старом синтаксисе, можно сделать и опциями (во внутреннем представлении в ядре оно так и есть). Таким образом, следующие формы записи эквивалентны:
ipfw add allow tcp from 1.1.1.1,2.2.2.2 to not me in ipfw add allow proto tcp { src-ip 1.1.1.1 or src-ip 2.2.2.2 } not dst-ip me in
и соответствуют логическому выражению:
(протокол = tcp ?) И ((src-адрес = 1.1.1.1 ?) ИЛИ (src-адрес = 2.2.2.2 ?)) И (НЕ (dst-адрес = любой мой адрес ?)) И (пакет проверятся на входном проходе ?)
То есть, здесь для каждого предиката проверяется его истинность, и из них из всех вычисляется истинность или ложность всего логического выражения. Следует отметить, что, поскольку это КНФ, "НЕ" (not) может быть применен только к самому предикату, а не их группе, то есть, "to not me 445" будет означать "(НЕ (dst-адрес = любой мой адрес ?)) И (порт назначения = 445 ?)", но не "НЕ ((dst-адрес = любой мой адрес ?) И (порт назначения = 445 ?))", а форма вида "not { ... or ...}" вообще недопустима. К слову, реально вычисление OR-блока идет слева направо, и при первом же истинном предикате (или его отрицании) все остальные внутри OR-блока не вычисляются (может быть полезно для оптимизации).
Однако вычисление предиката для пакета - операция, занимающая какое-то время, и при большом числе правил и пакетов оно может стать существенным. Здесь-то как раз и может придти на помощь skipto. Пример из жизни - в исследовательских целях было написано несколько сот правил вида:
add 120 count log ip from 1.1.1.1 to any in via int0 ipttl 63,65-127,129-255 add 120 count log ip from 1.1.1.2 to any in via int0 ipttl 63,65-127,129-255 add 120 count log ip from 1.1.1.3 to any in via int0 ipttl 63,65-127,129-255 add 120 count log ip from 1.1.1.4 to any in via int0 ipttl 63,65-127,129-255 ...
Видно, что правила указаны вполне точно, направление, адрес, интерфейс - но получается много повторений, и проверяться будет каждый пакет, в то время как подпадающих под условие пакетов - не так много. Простое добавление 119 правилом skipto 121 с условием, не совпадающим с повторящимися частями, привело к вполне заметному невооруженным глазом снижению нагрузки на процессор процентов на 5-10 (точные замеры не проводились)! А всю конструкцию можно было таким манером оптимизировать еще больше, убрав повторяющиеся части:
add 119 skipto 121 { not in or not recv int0 or not ipttl 63,65-127,129-255 } add 120 count log ip from 1.1.1.1 to any add 120 count log ip from 1.1.1.2 to any add 120 count log ip from 1.1.1.3 to any add 120 count log ip from 1.1.1.4 to any ...
Разумеется, такой частный случай оптимизации по скорости - не единственное применение skipto. Многие, к примеру, рекомендуют разделять пакет по направлению и интерфейсу наподобие вот такого:
ipfw add 10 deny tcp from any to any 135,445 // блокируем всегда ipfw add 20 allow tcp from any to any 22 // доступ к роутеру на всякий случай ipfw add 100 skipto 1000 all from any to any in recv int0 ipfw add 200 skipto 2000 all from any to any out xmit int0 ipfw add 300 skipto 3000 all from any to any in recv ext0 ipfw add 400 skipto 4000 all from any to any out xmit ext0 ipfw add 1000 ... // все пакеты в этой точке и далее будут для in recv int0 ... // поэтому к правилам здесь это можно не приписывать ipfw add 1999 allow ip from any to any // дефолтная политика для входящих int0 ipfw add 2000 ... // здесь будут пакеты, уходящие с интерфейса int0 ... и т.д.
Использование такого набора правил позволит всегда четко знать, в какой части рулесета с какими характеристиками проходит пакет. Ман-страница ipfw, кроме того, настоятельно рекомендует выполнить такое разделение для пакетов на layer2 (уровень фреймов Ethernet) - когда включен соответствующий sysctl, пакет, проходящий через роутер, попадает в ipfw_chk() уже не два раза, а ЧЕТЫРЕ (два на входе и два на выходе), из соответствующих ether_* функций на первой схеме. Причем предикаты для второго уровня будут проверяться и на обычных проходах в ip_input()/ip_output() - просто они всегда будут ложными. Но вот отрицания их всегда будут ложными, и здесь очень легко ошибиться в правилах с чем-нибудь вроде not MAC 10:20:30:40:50:60 any - так что проверки второго уровня лучше выделить в отдельные правила, не смешивая их с проверками на более высоких уровнях, и завершить allow all from any to any для L2-прохода. Таким образом увеличится и производительность - пакеты на L2 не будут лишний раз прогоняться по всем IP-правилам, обычно для L2 пишут совсем небольшое количество правил, они быстро выполнятся, и пакет продолжит путь дальше.
ДИНАМИЧЕСКИЕ ПРАВИЛА и STATEFUL FIREWALL.
И в заключение следует рассмотреть еще одну сложную тему. Как известно, одним из принципов при проектировании протоколов Internet являлся "состояние должно храниться во взаимодействующих машинах, а не в самой сети", что является гарантией того, что сбои где-то в сети между хостами будут иметь на них минимально возможное влияние. Разнообразные NATы и файрволы с отслеживанием состояния нарушают этот принцип. Кроме того, отслеживание соединений ведет к увеличению нагрузки (в том числе по памяти) на маршрутизатор в зависимости от количества активных соединений, и его, в отличие от аналогичного без хранения состояния, нередко можно "зафлудить" запросами на соединение. Однако за все надо платить, и некоторые вещи невозможно сделать без нормального отслеживания состояния. Скажем, в ipfw есть опции setup и established для tcp-соединений, которые просто смотрят на соответствующие флаги в tcp-пакете - просто и быстро, правило с established в начале списка правил может весьма ускорить работу файрвола ввиду отсутствия необходимости проверять дальнейшие правила для основной массы пакетов. Но таким образом нельзя организовать отслеживание для других протоколов (не tcp), да и хакеру никто не мешает передавать данные в пакетах без флагов с помощью специальных программ - пакеты беспрепятственно пройдут через такое правило с established, и до остальных запрещающих просто не дойдут.
Поэтому в ipfw была добавлена поддержка отслеживания состояний (stateful firewall), называемая динамическими правилами. В соответствии с озвученным выше принципом, она была именно добавлена, то есть администратор может использовать обычную попакетную фильтрацию, и в строго определенных, нужных ему точках, добавлять проверку состояния (отступление: это называется сохранением состояния, потому что файрвол сохраняет данные о соединениях и "помнит" их между пакетами, тогда как в обычном режиме, показанном на схемах выше, вычисление для каждого пакета начинается заново, вне зависимости от других пакетов).
Реализуется эта поддержка ключевыми словами check-state, keep-state и limit. Дальше мы мы будет рассматривать только keep-state, потому что правила с limit отличаются только тем, что налагают ограничение на число записей в таблице динамических правил, одновременно подпадающих под указанное ограничение - текущая реализация в ipfw2 при попытке создать новую запись просто молча уничтожает пакет (применяет deny).
Отдельно от обычных правил, называемых теперь статическими, в ядре заводится таблица динамических правил (ее текущее содержимое можно посмотреть по ipfw -d show), над которой возможны две операции: создание записи (динамического правило) на основе информации в пакете и проверка пакета на соответствие таблице - есть ли подходящие ему записи. Запись имеет вид: "протокол адрес1:порт1 <-> адрес2:порт2 ссылка_на_родительское_правило". Чтобы пакеты соединения в обоих направлениях подпадали под одно и то же динамическое правило, направление в нем не учитывается - то есть, должен совпасть протокол и обе пары адресов и портов, но пары можно менять местами: udp-пакеты с 1.2.3.4:5678 на 6.7.8.9:1234 и с 6.7.8.9:1234 на 1.2.3.4:5678 - оба подпадут под одно и то же правило.
Этим двум операциям и соответствуют ключевые слова check-state и keep-state. Причем, поскольку администратору необязательно указывать в правилах явный check-state (либо он может быть "перепрыгнут" каким либо правилом skipto до него), то в _каждое_ правило с keep-state неявно добавляется "невидимый" check-state - это сделано затем, чтоб избежать попыток добавить в таблицу динамических правил такое правило, которое там уже есть.
Как можно видеть из схемы, каждая запись в таблице динамических правил содержит ссылку на так называемое родительское правило - то, которое его сгенерировало по keep-state. И при поиске соответствия полей пакета записям в таблице динамических правил производится переход на часть действия указанного родительского правила - с этого момента динамические правила как бы перестают существовать, пакет привычным образом продолжает движение по статическому набору правил, просто был сделан своеобразный skipto (хоть и не на само правило, а на его кусок).
Что из этого следует? А то, что если действие в правиле каким-либо образом предполагает дальнейшую обработку в файрволе, например это divert или pipe, то пакет продолжит свое движение по правилам! Но наиболее интересен случай, когда действием является skipto. В этом случае для пакетов в оба направления (принадлежащих соединению) можно организовывать своего рода "подпрограммы" в любом месте набора правил, применять к ним несколько действий - например, отправить в pipe/queue, а потом часть пакетов запретить, другую разрешить (по критериям, отличным от изначального условия создания динамического правила).
Тот факт, что на самом деле "перепрыгивание" выполняется на параметры действия, позволяет использовать это для интересных вещей. В частности, с использованием появившегося во FreeBSD 6.2 параметра tag на каждый пакет можно навешивать внутриядерный тег, что в применении со skipto позволяет сделать, к примеру, запоминание, с какого шлюза пришел входящий пакет на машине с каналами к двум разным провайдерам, и ответные пакеты отправлять в тот канал, откуда они пришли (допустим, у вашей машины только один IP-адрес, и сделать fwd на базе внешнего адреса не получится), т.е. реализовать аналог reply-to из pf:
ipfw add 100 skipto 300 tag 1 in recv $ext_if1 keep-state ipfw add 200 skipto 300 tag 2 in recv $ext_if2 keep-state ipfw add 300 allow { recv $ext_if1 or recv $ext_if2 } # входящие снаружи ipfw add 400 allow in recv $int_if # разрешить ответы на внутреннем проходе ipfw add 500 fwd $gw1 tagged 1 # остались ответы на внешнем интерфейсе, ipfw add 600 fwd $gw2 tagged 2 # зарулим их куда надо
Следующий пример, взятый из реального up-script'а mpd, хоть и несколько запутан, но показывает, каким образом можно организовать на внешнем интерфейсе одновременно NAT для внутренней сети (выпуская только тех пользователей, которым это разрешено), ограничение полосы пропускания для каждого, лимит одновременных соединений для каждого пользвателя (динамические правила), причем с разными лимитами для HTTP-трафика и всего остального, и всё это - в минимальном количестве правил:
# mpd up-script args vars eif=$1 our_ip=$3 fw="/sbin/ipfw -q add"
# first split traffic to "incoming" and "outgoing from allowed hosts" $fw 161 skipto 166 src-ip table\($allowed_int_hosts\) out xmit $eif $fw 161 divert natd all from any to $our_ip in recv $eif $fw 163 queue 1 ip from any to any in via $eif $fw 164 allow ip from any to any in via $eif # deny world inet by default, both incoming and outgoing $fw 165 deny not src-ip $our_ip not dst-ip $our_ip via $eif # don't allow users to open more than 9 WWW connects and 6 for other protocols $fw 166 skipto 167 tcp from any to any 80 out xmit $eif recv int0 limit src-addr 9 $fw 166 skipto 167 all from any to any out xmit $eif recv int0 limit src-addr 6 # we are using here (undocumented) that 'limit' will drop overlimit packets, # not go to next rule $fw 167 divert natd ip from any to any out xmit $eif $fw 168 allow ip from $our_ip to any via $eif
Таким образом, в сложных конфигурациях, где, например, требуется фильтровать трафик как до трансляции, так и после, становится понятно, почему в ipfw, в отличие от других файрволов, нет жесткой схемы прохождения пакетов, вида "сначала NAT, потом фильтрация", почему низкоуровневая обработка позволяет делать более гибкие вещи. Платить за это приходится, разумеется, повышенной сложностью написания и понимания таких наборов правил, но в ситуациях, где конфигурации сложны и без того, это становится не столь существенно, и на первый план выходит сама возможность сделать задачу - и гибкость ipfw это позволяет.