packet filtered что значит
BPF для самых маленьких, часть нулевая: classic BPF
Berkeley Packet Filters (BPF) — это технология ядра Linux, которая не сходит с первых полос англоязычных технических изданий вот уже несколько лет подряд. Конференции забиты докладами про использование и разработку BPF. David Miller, мантейнер сетевой подсистемы Linux, называет свой доклад на Linux Plumbers 2018 «This talk is not about XDP» (XDP – это один из вариантов использования BPF). Brendan Gregg читает доклады под названием Linux BPF Superpowers. Toke Høiland-Jørgensen смеется, что ядро это теперь microkernel. Thomas Graf рекламирует идею о том, что BPF — это javascript для ядра.
Разработка BPF контролируется сетевым сообществом Linux, основные существующие применения BPF связаны с сетями и поэтому, с позволения @eucariot, я назвал серию «BPF для самых маленьких», в честь великой серии «Сети для самых маленьких».
Краткий курс истории BPF(c)
В конце восьмидесятых годов прошлого века инженеры из знаменитой Lawrence Berkeley Laboratory заинтересовались вопросом о том, как правильно фильтровать сетевые пакеты на современном для конца восьмидесятых годов прошлого века железе. Базовая идея фильтрации, реализованная изначально в технологии CSPF (CMU/Stanford Packet Filter), состояла в том, чтобы фильтровать лишние пакеты как можно раньше, т.е. в пространстве ядра, так как это позволяет не копировать лишние данные в пространство пользователя. Чтобы обеспечить безопасность времени выполнения для запуска пользовательского кода в пространстве ядра, использовалась виртуальная машина — песочница.
Однако виртуальные машины для существовавших фильтров были спроектированы для запуска на машинах со стековой архитектурой и на новых RISC машинах работали не так эффективно. В итоге усилиями инженеров из Berkeley Labs была разработана новая технология BPF (Berkeley Packet Filters), архитектура виртуальной машины которой была спроектирована на основе процессора Motorola 6502 — рабочей лошадки таких известных продуктов как Apple II или NES. Новая виртуальная машина увеличивала производительность фильтров в десятки раз по сравнению с существовавшими решениями.
Архитектура машины BPF
Общая схема запуска машины следующая. Пользователь создает программу для архитектуры BPF и, при помощи какого-то механизма ядра (например, системного вызова), загружает и подключает программу к какому-то генератору событий в ядре (например, событие — это приход очередного пакета на сетевую карту). При возникновении события ядро запускает программу (например, в интерпретаторе), при этом память машины соответствует какому-то региону памяти ядра (например, данным пришедшего пакета).
tcpdump
Пример: наблюдаем IPv6 пакеты
Пример посложнее: смотрим на TCP пакеты по порту назначения
Посмотрим как выглядит фильтр, который копирует все TCP пакеты с портом назначения 666. Мы рассмотрим случай IPv4, так как случай IPv6 проще. После изучения данного примера, вы можете в качестве упражнения самостоятельно изучить фильтр для IPv6 ( ip6 and tcp dst port 666 ) и фильтр для общего случая ( tcp dst port 666 ). Итак, интересующий нас фильтр выглядит следующим образом:
Что делают строчки 0 и 1 мы уже знаем. На строке 2 мы уже проверили, что это IPv4 пакет (Ether Type = 0x800 ) и загружаем в регистр A 24-й байт пакета. Наш пакет выглядит как
а значит мы загружаем в регистр A поле Protocol заголовка IP, что логично, ведь мы же хотим копировать только TCP пакеты. Мы сравниваем Protocol с 0x6 ( IPPROTO_TCP ) на строке 3.
На строках 4 и 5 мы загружаем полслова, находящиеся по адресу 20, и при помощи команды jset проверяем, не выставлен ли один из трех флагов — в маске выданной jset очищены три старшие бита. Два бита из трех говорят нам, является ли пакет частью фрагментированного IP пакета, и если да, то является ли он последним фрагментом. Третий бит зарезервирован и должен быть равен нулю. Мы не хотим проверять ни нецелые ни битые пакеты, поэтому и проверяем все три бита.
Наконец, на строке 8 мы сравниваем порт назначения с искомым значением и на строчках 9 или 10 возвращаем результат — копировать пакет или нет.
Tcpdump: загрузка
Для того, чтобы посмотреть как функция pcap_setfilter реализована в Linux, мы используем strace (некоторые строчки были удалены):
Стоит отметить, что в классическом BPF загрузка и подсоединение фильтра всегда происходит как атомарная операция, а в новой версии BPF загрузка программы и привязка ее к генератору событий разделены по времени.
Чуть более полная версия вывода выглядит так:
Как было сказано выше, мы загружаем и подсоединяем к сокету наш фильтр на строке 5, но что происходит на строчках 3 и 4? Оказывается, это libpcap заботится о нас — для того, чтобы в вывод нашего фильтра не попали пакеты, ему не удовлетворяющие, библиотека подсоединяет фиктивный фильтр ret #0 (дропнуть все пакеты), переводит сокет в неблокирующий режим и пытается вычитать все пакеты, которые могли остаться от прошлых фильтров.
Интересно, что фильтр можно присоединять к любому сокету, не только к raw. Вот пример программы, которая отрезает все, кроме двух первых байт у всех входящих UDP датаграмм. (Комментарии я добавил в коде, чтобы не загромождать статью.)
Подробнее про использование setsockopt для подсоединения фильтров см. в socket(7), а про написание своих фильтров вида struct sock_fprog без помощи tcpdump мы поговорим в разделе Программируем BPF при помощи собственных рук.
Классический BPF и XXI век
BPF был включен в Linux в 1997 году и долгое время оставался рабочей лошадкой libpcap без особых изменений (Linux-специфичные изменения, конечно, были, но они не меняли глобальной картины). Первые серьезные признаки того, что BPF будет эволюционировать появились в 2011 году, когда Eric Dumazet предложил патч, добавляющий в ядро Just In Time Compiler — транслятор для перевода байткода BPF в нативный x86_64 код.
Мы скоро рассмотрим все эти примеры подробнее, однако сначала нам будет полезно научиться писать и компилировать произвольные программы для BPF, так как возможности, предоставляемые библиотекой libpcap ограничены (простой пример: фильтр, сгенерированный libpcap может вернуть только два значения — 0 или 0x40000) или вообще, как в случае seccomp, неприменимы.
Программируем BPF при помощи собственных рук
Познакомимся с бинарным форматом инструкций BPF, он очень простой:
а целая программа — в виде структуры
Таким образом, мы уже можем писать программы (коды инструкций мы, допустим, знаем из [1]). Вот так будет выглядеть фильтр ip6 из нашего первого примера:
Программу prog мы можем легально использовать в вызове
Однако, и такой вариант не очень-то удобен. Так рассудили и программисты ядра Linux и поэтому в директории tools/bpf ядра можно найти ассемблер и дебагер для работы с классическим BPF.
Для удобства C программистов можно использовать другой формат вывода:
Расширения Linux и netsniff-ng
seccomp
Итак, мы уже умеем писать BPF программы произвольной сложности и готовы посмотреть на новые примеры, первый из которых — это технология seccomp, позволяющая при помощи фильтров BPF управлять множеством и набором аргументов системных вызовов, доступных данному процессу и его потомкам.
Отметим, что на хабре уже были статьи про использование seccomp, может кому-то захочется прочитать их до (или вместо) чтения следующих подразделов. В статье Контейнеры и безопасность: seccomp приведены примеры использования seccomp, как версии 2007 года, так и версии с использованием BPF (фильтры генерируются при помощи libseccomp), рассказывается про связь seccomp с Docker, а также приведено много полезных ссылок. В статье Изолируем демоны с systemd или «вам не нужен Docker для этого!» рассказывается, в частности, о том, как добавлять черные или белые списки системных вызовов для демонов под управлением systemd.
Пишем и загружаем фильтры для seccomp
Мы уже умеем писать BPF программы и поэтому посмотрим сначала на программный интерфейс seccomp. Установить фильтр можно на уровне процесса, при этом все дочерние процессы будут ограничения наследовать. Делается это при помощи системного вызова seccomp(2) :
Чем же отличаются программы для seccomp от программ для сокетов? Передаваемым контекстом. В случае сокетов, нам передавалась область памяти, содержащая пакет, а в случае seccomp нам передается структура вида
Здесь nr — это номер запускаемого системного вызова, arch — текущая архитектура (об этом ниже), args — до шести аргументов системного вызова, а instruction_pointer — это указатель на инструкцию в пространстве пользователя, которая сделала данный системный вызов. Таким образом, например, чтобы загрузить номер системного вызова в регистр A мы должны сказать
В принципе, мы уже знаем все, чтобы писать и читать seccomp программы. Обычно логика программы устроена как белый или черный список системных вызовов, например программа
проверяет черный список из четырех системных вызовов под номерами 304, 176, 239, 279. Что же это за системные вызовы? Мы не можем сказать точно, так как мы не знаем для какой архитектуры писалась программа. Поэтому авторы seccomp предлагают начинать все программы с проверки архитектуры (текущая архитектура указывается в контексте как поле arch структуры struct seccomp_data ). С проверкой архитектуры начало примера выглядело бы как:
и тогда наши номера системных вызовов получили бы определенные значения.
Пишем и загружаем фильтры для seccomp при помощи libseccomp
Написание фильтров в машинных кодах или для ассемблера BPF позволяет получить полный контроль над результатом, но в то же время иногда предпочтительнее иметь переносимый и/или читаемый код. В этом нам поможет библиотека libseccomp, предоставляющая стандартный интерфейс для написания черных или белых фильтров.
Давайте, например, напишем программу, которая запускает бинарный файл по выбору пользователя, установив, предварительно, черный список системных вызовов из вышеупомянутой статьи (программа упрощена для большей читаемости, полный вариант можно найти тут):
Пример успешного запуска:
Пример заблокированного системного вызова:
Если хотите посмотреть как устроены фильтры с бинарным поиском, то взгляните на простой скрипт, генерирующий такие программы на ассемблере BPF по набору номеров системных вызовов, например:
Ничего существенно более быстрое написать не получится, так как программы BPF не могут совершать переходы по отступу (мы не можем сделать, например, jmp A или jmp [label+X] ) и поэтому все переходы статические.
seccomp и strace
отрабатывают за примерно одно и то же время, хотя во втором случае мы хотим трейсить только один системный вызов.
Чуть больше подробностей о том как именно strace работает с seccomp можно узнать из недавнего доклада. Для нас же наиболее интересным фактом является то, что классический BPF в лице seccomp находит применения до сих пор.
xt_bpf
Отправимся теперь обратно в мир сетей.
Предыстория: давным-давно, в 2007 году, в ядро был добавлен модуль xt_u32 для netfilter. Он был написан по аналогии с еще более древним классификатором трафика cls_u32 и позволял писать произвольные бинарные правила для iptables при помощи следующих простых операций: загрузить 32 бита из пакета и проделать с ними набор арифметических операций. Например,
Загружает 32 бита заголовка IP, начиная с отступа 6, и применяет к ним маску 0xFF (взять младший байт). Это — поле protocol заголовка IP и мы его сравниваем с 1 (ICMP). В одном правиле можно комбинировать много проверок, а еще можно выполнять оператор @ — перейти на X байт вправо. Например, правило
здесь — это код в формате вывода ассемблера bpf_asm по-умолчанию, например,
Понятно, что модуль xt_bpf поддерживает более сложные фильтры, чем в примере выше. Давайте посмотрим на настоящие примеры от компании Cloudfare. До недавнего времени они использовали модуль xt_bpf для защиты от DDoS атак. В статье Introducing the BPF Tools они рассказывают как (и почему) они генерируют BPF фильтры и публикуют ссылки на набор утилит для создания таких фильтров. Например, при помощи утилиты bpfgen можно создать BPF программу, которая матчит DNS-запрос на имя habr.com :
В программе мы сначала загружаем в регистр X адрес начала строки \x04habr\x03com\x00 внутри UDP-датаграммы и потом проверяем запрос: 0x04686162 «\x04hab» и т.д.
cls_bpf
Еще одна причина не рассказывать об использовании классического BPF c cls_bpf заключается в том, что по сравнению с Extended BPF в этом случае кардинально сужается область применимости: классические программы не могут менять содержимое пакетов и не могут сохранять состояние между вызовами.
Так что пришло время попрощаться с классическим BPF и заглянуть в будущее.
Прощание с classic BPF
Мы посмотрели на то, как технология BPF, разработанная в начале девяностых успешно прожила четверть века и до конца находила новые применения. Однако, подобно переходу со стековых машин на RISC, послужившему толчком к разработке классического BPF, в двухтысячные случился переход с 32-битных на 64-битные машины и классический BPF стал устаревать. Кроме этого, возможности классического BPF сильно ограничены и помимо устаревшей архитектуры — у нас нет возможности сохранять состояние между вызовами BPF программ, нет возможности прямого взаимодействия с пользователем, нет возможности взаимодействия с ядром, кроме чтения ограниченного количества полей структуры sk_buff и запуска простейших функций-помощников, нельзя изменять содержимое пакетов и переадресовывать их.
На самом деле, в настоящее время от классического BPF в Linux остался только API интерфейс, а внутри ядра все классические программы, будь то фильтры сокетов или фильтры seccomp, автоматически транслируются в новый формат, Extended BPF. (Мы расскажем про то как именно это происходит в следующей статье.)
Переход на новую архитектуру начался в 2013 году, когда Алексей Старовойтов предложил схему обновления BPF. В 2014 году соответствующие патчи стали появляться в ядре. Насколько я понимаю, изначально планировалось лишь оптимизировать архитектуру и JIT-compiler для более эффективной работы на 64-битных машинах, но вместо этого эти оптимизации положили начало новой главе в разработке Linux.
Дальнейшие статьи в этой серии расскажут об архитектуре и применениях новой технологии, изначально известной как internal BPF, затем extended BPF, а теперь как просто BPF.
Выполняю установку, настройку, сопровождение серверов. Для уточнения деталей используйте форму обратной связи
Обновлена 23.06.2016
Введение.
Пару слов о самом pf. Пакетный фильтр (далее PF) OpenBSD предназначен для фильтрации TCP/IP трафика и трансляции адресов (NAT). PF так же способен нормализовать и преобразовывать TCP/IP трафик, управлять приоритетами пакетов и пропускной способностью. PF был включен в ядро GENERIC OpenBSD, начиная с OpenBSD 3.0. Предшествующие версии OpenBSD использовали другой файрвол/NAT пакет, который более не поддерживается. Позже он портировался на FreBSD (начиная с 5.3) и Solaris (начиная с Oracle Solaris 11.3)
PF изначально был разработан Даниэлем Хартмаером (Daniel Hartmeier), а теперь поддерживается и разрабатывается Даниелем и остальной командой OpenBSD.
Особенности pf
Зараннее отмечу пару фактов о фаерволле pf, которые обязательно надо иметь в виду.
PF — поддерживает SMP только во FreBSD (начиная с 10-ой версии), то есть можно распараллелить его работу на несколько ядер CPU.
Механизм altq работает только для исходящего траффика. Это действительно так, но эта особенность нам мешать не будет, поскольку мы сделаем так, как было упомянуто выше: применим altq к исходящему траффику на внутреннем интерфейсе (а не к входящему на внешнем).
В секцию фильтров пакеты попадают после обработки NAT-ом. Это тоже действительно так. Чем это может нам помешать? А тем, что по нужным очередям пакеты рассовываются именно в секции фильтров, а рассовывая их, нам надо знать, от какого пользователя (читай: с какого адреса) они пришли на шлюз. Но эта особенность нам мешать тоже не будет, поскольку фильтры мы определим для внутреннего интерфейса (через который пакеты приходят еще не обработанные NAT-ом). При этом интересно обратить внимание на то, что фильтр будет применяться к пакету на одном интерфейсе, и засовывать его в очередь другого интерфейса. (Такой подход в самом деле неочевиден — в рассылках встречались вопросы по pf вроде «зачем позволять присваивать очередям входящий траффик, если очереди работают только для исходящего» или «зачем позволять присваивать пакеты, проходящие через один интерфейс, в очередь на другом».)
При использовании keep-state пользовательскими фильтрами обрабатывается только первый (state-creating) пакет. Это мешает нам тем, что если пользователь отправляет один запрос, то он, как исходящий, становится в соответствующую очередь. Но когда пользователю приходит ответ, то при использовании keep-state таблица правил не просматривается, — вместо этого к новому пакету применяются те же правила, что и к первому. То есть он становится в очередь для исходящего траффика на внешнем интерфейсе — а это совсем не то, что нам требуется. Однако, эта особенность, опять же, мешать не будет — мы откажемся от keep-state.
Трудно поверить, но стройность этих теоретических рассуждений не подвела и на практике.
1) Установка pf.
Здесь можно пойти 2-мя путями: либо загрузить модулями либо пересобрать ядро. Я опишу оба способа, а вы же, выбирайте тот, который вам больше подходит.
— модулями (это лучше делать тогда, когда сервер физически удалён или нет возможности пересобрать ядро)
Здесь нам понадобиться подгрузить такие модули :
, отвечает за загрузку собственно pf
, отвечает за возможность логгирования пакетов.
Замечу, что pf при включении лоялнее чем, например, тот же ipfw. Как это сказывается? Если вы работали с ipfw, то сразу вспомните, что при загрузке модуля этот файервол находится в режиме deny all (блокировать всё). pf — пошёл немного по другому пути. При загрузке модулем ничего не происходит. Что бы задействовать его, нужно выполнить команду (о ней речь пойдёт немного позже) и после этого файервол переходит в режим permit all (разрешено всё). Поэтому, многие мои знакомые любят pf именно за эту особенность, как врочем и я.
После загрузки модуля нужно задействовать его:
— пересобрать ядро (работает быстрее, чем когда подгружен модулем).
Что бы пересобрать ядро с поддержкой pf, нужно добавить такое в конфигурационный файл ядра:
device pf
device pflog
а если хотите использовать возможность использовать ограничения по скоростям, шейпинг, то нужно добавить ещё и такое:
options ALTQ
options ALTQ_CBQ # Class Bases Queuing (CBQ)
options ALTQ_RED # Random Early Detection (RED)
options ALTQ_RIO # RED In/Out
options ALTQ_HFSC # Hierarchical Packet Scheduler (HFSC)
options ALTQ_PRIQ # Priority Queuing (PRIQ)
options ALTQ_NOPCC # Required for SMP build
options ALTQ_DEBUG
После этого пересобираем ядро.
Хочу добавить ещё один немаловажный факт. Независимо от того, каким вы способом добавили поддержку pf нужно ОБЯЗАТЕЛЬНО добавить следующие строчки в /etc/rc.conf ( ИНАЧЕ PF НЕ БУДЕТ ВКЛЮЧЁН )
pf_enable=»YES»
pf_rules=»/etc/pf.rules»
pflog_enable=»YES»
Первая строчка включает собственно файервол, вторая — указывает на местополжение файла с правилами, ну а третья добавляет возможность логгирования. Напомню ещё раз: даже если вы добавили поддержку pf в ядро, без этих строчек он не будет задействован (в отличии от того же ipfw), поэтому, не забывайте об этом факте.
2) Начало работы.
И так, вы успешно добавили поддержку файервола pf в ядро. Как же с ним работать? Сразу после задействования pf, он не содержит ни одного правила, то есть просто «наблюдает» за пакетами.
Для управления файерволом pf существует исполняемый файл pfctl. Этот же бинарник отвечает и за включение/отключение файервола. Ниже приведено несколько примеров использования:
Теперь переходим к тому, как же добавлять, удалять, просматривать правила. Те, кто работал с ipfw увидят существенные отличия. Какие именно? Об этом ниже.
Первое : правила загружать можно только из файла
Второе : если при загрузке правил встретилась ошибка, то не применяются ВСЕ правила, то есть остаются те, которые были до измнений (в отличии от ipfw, в котором правила применяются, а на ошибке останавливается).
Есть и остальные моменты, но на мой взгляд пока только эти нужны новичкам.
Теперь перейдём к более тесному общению с утилитой pfctl. Она отвечает за управление файерволом pf. Что бы успешно работать с pf нужно знать как им управлять. И так, ниже будет описано несколько часто используемых опций pfctl:
-d — выключить pf
-e — включить pf
-f file — загрузить правила из файла file
-g — включить «помогающий» вывод (используется при отладке)
-n — не загружать правила, только проверить на ошибки
-q — вывод только ошибок и предупреждений
-s modifier — просмотр параметра, указанного в modifier (то есть можно посмотреть правил NAT, трянсляции, статистики, …)
-v — широкий вывод (так называемый ружим verbose), позволяет посмотреть статистику по каждому правилу
Это те параметры, которые вам понадобятся на первое время. Остальные же (а так же детально и те, которые были описаны выше) можно узнать из справочного руководства man pfctl.
3) Пишем правила.
Сразу скажу, что в pf порядок правил строго регламентирован, что означает, что они должны идти в определённом порядке. То есть нельзя сначала написать правила фильтрации, потом правила трансляции или смешивать между собой. Если же вы попытаетесь это сделать, то получите ошибку:
/etc/pf.rules:14: Rules must be in order: options, normalization, queueing, translation, filtering
pfctl: Syntax error in config file: pf rules not loaded
Правила в pf разделены на 7 частей:
a) Макросы: Определенные пользователем переменные, которые могут содержать IP адреса, имена интерфейсов и т.п.
b) Таблицы: Структуры используемые для хранения списков IP адресов.
c) Опции: Различные опции для управления работой PF.
d) Скраб: Пересборка пакетов для их нормализации и дефрагментации.
e) Очереди: Управление пропускной способностью и приоритетами пакетов (ALTQ).
f) Преобразования: Управление преобразованием сетевых адресов и перенаправлением пакетов (NAT, проброс портов).
g) Правила фильтрации: Позволяет выборочно фильтровать или блокировать пакеты, прошедшие через любой интерфейс.
За исключение макросов и таблиц, каждая часть в конфигурационном файле должна следовать в указанном порядке, хотя не обязательно должны присутствовать все части. Пустые строки игнорируются, а строки начинающиеся с # считаются коментариями.
А теперь пришло время писать правила.
Файерволы, в основном используются для фильтрации трафика, а точнее — для фильтрации пакетов. Фильтрация пакетов — это выборочное разрешение или запрещение прохождения пакетов данных через сетевой интерфейс. Параметры, которые использует PF(4) когда проверят пакеты, базируются на заголовках Layer 3 (IPv4 и IPv6) и Layer 4 (TCP, UDP, ICMP, и ICMPv6). Наиболее часто используются такие параметры как: адрес источника и назначения, порт источника и назначения, а также протокол. Правила фильтрации описывают параметры, которым должен соответствовать пакет, а также результирующее действие, которое производится когда соответствие найдено.
Главное отличие pf от «классического» файервола состоит в том, что применяется не первое правило, а последнее. Иными словами, если пакет удовлетворяет 3 правилам, то применится правило, которое располагается ниже остальных. Но! Если вы всё-таки привыкли к «классическому» понятию файервола, то разработчики предусмотрели и эту ситуацию. Что бы применялось первое правило — достаточно в него добавить ключевое слово quick.
Общий вид правлила таков (на самом деле я немного упростил правило, убрав параметры флагов и состояния):
action [direction] [log] [quick] [on interface] [af] [proto protocol] [from src_addr [port src_port]] [to dst_addr [port dst_port]]
Кратко, о том, что означают каждый из параметров.
Действие предпринимаемое к подходящим пакетам, либо pass, либо block. Действие pass будет пропускать пакеты в ядро для дальнейшей обработки, в то время как действие block будет реагировать так — как указано в опции block-policy. Значение по умолчанию которой можно изменить на block drop или block return.
Направление движения пакета через интерфейс, либо in, либо out.
Означает что пакет должен быть зарегистрирован в pflogd(8). Если правило написано с опциями keep state, modulate state, или synproxy state, то регистрируется только тот пакет который создает это состояние (state). Для регистрации всех подряд пакетов, используйте log (all).
Если пакет соответствует правилу написанному с quick, то это правило считается последним соответствующим правилом и производится описанное действие этого правила.
Имя или группа сетевого интерфейсачерез который проходит пакет. Группа интерфейсов определяется именем интерфейса, но без цифрового значения. Например: ppp или fxp. Это заставит правило соответствовать любому пакету проходящему через любой ppp или fxp интерфейс, соответственно.
Семейство адресов пакета, либо inet для IPv4, либо inet6 для IPv6. PF обычно в состоянии определить этот параметр на основании адреса источника и/или назначения.
Протокол пакета 4-го уровня:
Любое значение из: tcp, udp, icmp, icmp6, правильное имя протокола из /etc/protocols, номер протокола между 0 и 255, несколько протоколов с использованием списка.
Адрес источника/приемника в заголовке IP.
Порт источника/приемника в заголовке пакета 4-го уровня.
Главный плюс (а может быть и минус) состоит в том, что правила можно сокращать, то есть опускать параметры, которые не используются. Например, правило
pass all from any to any
можно записать так:
Хочу заметить, что в pf отсутствует ключевое слово me (но вместо него можно использовать IP адрес, имя сетевого интерфейса), которое есть в ipfw и заменяет собой все свои IP-адреса. Это не очень удобно, так как если у вас несколько IP-адресов, то нужно указывать каждый из них (или для каждого из них писать правило).
4) Грамматика PF
Грамматика Пакетного Фильтра достаточно гибка и обеспечивает большую гибкость в написании правил. PF способен делать выводы из определённых ключевых слов, что означает, что они не должны быть чётко заданы в определённом стиле и определённом порядке и поэтому нет необходимости строго запоминать синтаксис.
Избавление от ключевых слов
Для определения политики по умолчанию, используются два правила:
block in all
block out all
Это может быть уменьшено до:
Когда не указано направление, PF будет считать, что правило применяется для пакетов направленных в обе стороны.
Подобным образом, выражения «from any to any» и «all» могут быть исключены из правил, например:
block in on rl0 all
pass in quick log on rl0 proto tcp from any to any port 22 keep state
может быть упрощено до:
block in on rl0
pass in quick log on rl0 proto tcp to port 22 keep state
Первое правило блокирует любые входящие пакеты на интерфейсе rl0, а второе правило впускает TCP трафик на rl0 порт 22.
Порядок ключевых слов
Порядок в котором указаны ключевые слова в большинстве случаев не важен. Например, правило записанное, как:
pass in log quick on rl0 proto tcp to port 22 flags S/SA keep state queue ssh label ssh
Может быть записано, как:
pass in quick log on rl0 proto tcp to port 22 queue ssh keep state label ssh flags S/SA
Другие, подобные варианты также будут работать.
Списки позволяют определять множества, имеющие общие признаки в пределах правила — такие как IP адреса, номера портов и т.д. Таким образом, вместо прописывания нескольких правил фильтрации для каждого IP адреса, который должен быть заблокирован, мы можем определить список IP адресов в пределах одного правила. Списки должны находиться внутри скобок <>.
Когда pfctl(8) доходит до списка при загрузке наборов правил, он раскладывает их на отдельные правила, для каждого элемента списка. Для примера:
block out on fxp0 from < 192.168.0.1, 10.5.32.6 >to any
Будет преобразован в:
block out on fxp0 from 192.168.0.1 to any
block out on fxp0 from 10.5.32.6 to any
Множественные списки могут применяться не только для блокировки:
rdr on fxp0 proto tcp from any to any port < 22 80 >-> 192.168.0.6
block out on fxp0 proto < tcp udp >from < 192.168.0.1, 10.5.32.6 >to any port
Стоит отметить, что запятая в списке является необязательной.
Макросы — определяемые пользователем переменные, которые могут держать IP адреса, номера портов, имена интерфейсов, и т.д. Макросы позволят облегчить написание наборов правил и сделают поддержание набора правил несравненно легче. Имена макросов должны начаться с символа и могут содержать символы, цифры, и символы подчеркивания. Названия макросов не могут носить имена зарезервированных слов, типа pass, out, или queue.
Это создаст макрос с именем ext_if. При использовании макроса, его имени должен предшествовать знак $. Макросы также могут быть расширены до списков.
Макросы могут определяться рекурсивно. В этом случае должен использоваться следующий синтаксис:
Теперь макрос $all_hosts расширен до значений 192.168.1.1, 192.168.1.2.
Таблицы используются для хранения группы адресов IPv6 и/или IPv4. Поиски в таблице занимают гораздо меньше времени и потребляют меньше ресурсов, чем списки. По этой причине, таблица идеальна чтобы хранить большую группу адресов, поскольку время поиска в таблице, содержащей 50 000 адресов — не намного больше чем для 50 адресов. Таблицы могут использоваться следующими способами:
* источник и/или адрес назначения для filter, scrub, NAT, и redirection rules
* адрес трансляции для правил NAT
* адрес переназначения для правил редиректа
* адрес назначения для правил фильтрации route-to, reply-to, и dup-to
Таблицы определяются в pf.conf или с помощью pfctl(8).
то здесь описано как с этим разобраться.
В pf.conf таблицы создаются используя директиву table. Следующие атрибуты могут быть определены для каждой таблицы:
* const — содержание таблицы не может быть изменено после ее создания. Когда этот атрибут не определен, pfctl(8) может использоваться, чтобы добавлять или удалять адреса из таблицы в любое время, при выполнении с securelevel(7), равным двум и выше.
* persist — заставляет ядро сохранять таблицу в памяти, даже когда никакие правила к ней не обращаются. Без этого атрибута, ядро автоматически удалит таблицу, когда последнее правило, ссылающееся на нее будет отработано.
Адреса могут также быть определены, используя модификатор типа отрицание (или «не»):
Таблица goodguys теперь содержит адреса сети 192.0.2.0/24, за исключением адреса 192.0.2.5. Обратите внимание, что имена таблицы всегда включаются в <>. Таблицы могут также могут заполняться из файлов, содержащих список адресов IP и сетей:
table persist file «/etc/spammers»
block in on fxp0 from to any
Файл /etc/spammers содержал бы список IP адресов или сетей CIDR. Любая строка начинающаяся с #, будет обработана как комментарий и проигнорирована.
6) Трансляция адресов (NAT)
Обратите внимание: Преобразуемые пакеты должны проходить через фильтр и будут блокированы или пропущены, в зависимости от фильтрующих правил, которые были заданы. Единственное исключение из этих правил, это когда используется ключевое слово pass с правилом nat. В этом случает пакеты проходящие NAT преобразования будут проходить без проверки правилами.
Так же помните, что трансляция происходит до фильтрации, фильтр будет видеть уже преобразованные пакеты с преобразованным IP адресом и портом
Основной формат правил NAT в pf.conf выглядит как:
ключевое слово, с которого начинается правило NAT
преобразованные пакеты не будут обрабатываются правилами фильтрации
логировать пакеты с помощью pflogd(8). Обычно только первый пакет заносится в журнал. Для логирования всех пакетов используйте log (all).
Название интерфейса, или группы интерфейсов на котором будут проводиться преобразования.
Семейство адресов, inet для IPv4 или inet6 для IPv6. PF как правило в сам в состоянии определить этот параметр c помощью исходных адресов и адресов назначения.
Исходные (внутренние) адреса пакетов, которые будут преобразованы. Исходные адреса могут быть указаны, как:
Единственный IPv4 или IPv6 адрес.
— Полное доменное имя, которое будет преобразовано через DNS сервер при загрузке правила. Полученные адреса окажутся в правиле.
— Название сетевого интерфейса или группы сетевых интерфейсов. Любые IP адреса принадлежащие интерфейсу будут подставлены в правило, во время загрузки.
— Название сетевого интерфейса сопровождающегося /netmask (например, /24). Каждый IP адрес на интерфейсе, совмещённый с сетевой маской, образует блок CIDR и оказывается в правиле.
— Название сетевого интерфейса или группы сетевых интерфейсов, сопровождающихся модификаторами:
:network — заменяется сетевым блоком CIDR (например, 192.168.0.0/24)
:broadcast — заменяется широковещательным адресом сети (например, 192.168.0.255)
:peer — заменяется peer IP адресом другой стороны point-to-point линка
Кроме того, модификатор :0 может быть добавлен к любому интерфейсу или к любому из вышеуказанных модификаторов, для указания, что PF не должен затрагивать alias IP адреса. Этот модификатор может использоваться, при указании интерфейса в круглых скобках. Пример: fxp0:network:0
Исходный порт в заголовке пакета. Порты могут быть указаны, как:
— Номер от 1 до 65535
— Актуальное название сервиса смотрите в /etc/services
— Набор портов, используя списки
— Диапазон:
Последние два бинарных оператора (они используют два аргумента) не включают аргументы в этот диапазон
Включающий диапазон, также бинарные операторы и включают аргументы в диапазон.
Опция port не часто используется в nat правилах, потому что обычно стоит задача преобразовывать весь трафик, не зависимо от используемых портов.
Адрес назначения преобразуемых пакетов. Адрес назначения указывается так же, как и исходный адрес.
Порт назначения. Порт указывается так же, как и исходный порт.
Внешний (преобразуемый) адрес на NAT шлюзе, в который будут пробразованы пакеты. Внешний адрес может быть указан как:
— Единственный IPv4 или IPv6 адрес.
— Сетевой блок CIDR
— Полное доменное имя, которое будет преобразовано через DNS сервер при загрузке правила. Полученные адреса окажутся в правиле.
— Название сетевого интерфейса. Любые IP адреса принадлежащие интерфейсу будут подставлены в правило, во время загрузки.
Название сетевого интерфейса указанного в круглых скобках ( ). Это говорит PF обновлять правило, если IP адрес(а) на указанном интерфейсе сменился. Полезно на интерфейсах, которые получают IP адреса по DHCP или используют dial-up, чтобы каждый раз при смене адреса не перегружать правила.
Название сетевого интерфейса, сопровождающееся одним из этих модификаторов:
:network — заменяется сетевым блоком CIDR (например, 192.168.0.0/24)
:peer — заменяется peer IP адресом другой стороны point-to-point линка
Кроме того, модификатор :0 может быть добавлен к любому интерфейсу или к любому из вышеуказанных модификаторов, для указания, что PF не должен задействовать alias IP адреса. Этот модификатор может использоваться при указании интерфейса в круглых скобках. Пример: fxp0:network:0
Ряд адресов, используя список.
Указывается тип диапазона адресов используемого для трансляции.
Не преобразовывать исходные порты в TCP и UDP пакетах. Как правило, для большинства случаев подойдёт что то вроде этого:
Эти правила говорят выполнять NAT на интерфейсе tl0 для любых входящих пакетов из 192.168.1.0/24 и заменять исходный IP адрес на 24.5.0.5. Не смотря на то, что вышеуказанное правило является корректным, использовать подобную форму не рекомендуется. Обслуживание может оказаться сложным, поскольку любое изменение чисел внешней или внутренней сети потребует изменения правил. Сравните с более простым в обслуживании правилом (tl0 внешний интерфейс, dc0, внутренний):
Преимущества на лицо : вы можете менять IP адреса на любом интерфейсе без изменения правила.
Когда указывается название интерфейса для трансляции адресов, как в примере выше, то IP адрес определяется в pf.conf во время загрузки, а не на «лету». Если вы используете DHCP для настройки ваших внешних интерфейсов, это может оказаться проблемой. Если ваши присваиваемые IP адреса меняются, NAT будет продолжать преобразовывать исходящие пакеты используя старый IP адрес. Это приведёт к остановке функционирования исходящих соединений. Чтобы этого избежать, вы можете сказать PF автоматически обновлять преобразуемый адрес, указав круглые скобки вокруг названия интерфейса:
Этот метод работает для преобразования IPv4 и IPv6 адресов.
Двунаправленное отображение (отображение 1:1)
Двунаправленное отображение может быть установлено использованием правила binat. Правило binat устанавливает один к одному отображение между внутренним IP адресом и внешним. Это может быть полезно, например, для предоставления веб сервера во внутренней сети со своим личным внешним IP адресом. Соединения из Интернета на внешний адрес будут транслироваться на внутренний адрес а соединения от веб сервера (такие как DNS запрос) будут преобразовываться во внешний адрес. TCP и UDP порты никогда не изменяются с binat правилом.
Исключения правил трансляции
Исключения в правилах трансляции могут быть сделаны используя ключевое слово no. Например, если изменить пример указанный выше, то он будет выглядеть так:
Тогда указанная сеть 192.168.1.0/24 будет транслироваться через внешний адрес 24.2.74.79, за исключением адреса 192.168.1.208.
Обратите внимание, что первое правило главнее; Если существует ключевое слово no тогда пакеты не транслируются. Ключевое слово no так же может быть использовано с binat и rdr правилами.
Проверка NAT статуса
Объяснение (только первая строка):
Отображает интерфейс к которому привязан стейт. Слово self будет отображаться если стейт плавающий floating.
Используемый соединением протокол.
IP адрес (192.168.1.35) машины во внутренней сети. Исходный порт (2132) показан после адреса. Также адрес, который находится в IP заголовке.
IP адрес (24.5.0.5) и порт (53136) на шлюзе, в который будут транслированы пакеты.
IP адрес (65.42.33.245) и порт (22) к которому внутренняя машина соединяется.
Показывает в каком состоянии прибывает TCP соединение
7) Перенаправление (Проброс портов)
Если у вас есть работающий NAT в вашем офисе, то вы имеете выход в Интернет на всех машинах. Что если у вас есть машина позади NAT шлюза, которая должна быть доступна из вне? Вот где вступает в работу перенаправление. Перенаправление позволяет входящему трафику быть посланным машине находящейся позади NAT шлюза.
Давайте рассмотрим пример:
Эта строка перенаправляет трафик входящий на 80 TCP порт (веб сервер) на машину внутри сети с ip 192.168.1.20. Поэтому, даже несмотря на то, что 192.168.1.20 находится за шлюзом и внутри вашей сети, внешний мир может иметь к этой машине доступ.
Часть правила from any to any может быть весьма полезна. Если вы знаете какой адрес или подсеть должны иметь доступ к веб серверу на 80 порт, то вы можете это указать:
Это будет перенаправлять только указанную подсеть. Обратите внимание, это означает, что вы можете перенаправлять определённые входящие хосты на определённые машины находящиеся за натом. Это может оказаться полезным. Например, вы можете давать удалённым пользователями доступ на их собственные рабочие компьютеры, если вы знаете кто с какого IP адреса будет соединяться:
Диапазон портов также может быть перенаправлен:
Эти примеры показывают перенаправление портов от 5000 до 5500 включительно, на машину 192.168.1.20. В правиле #1 порт 5000 перенаправляется на 5000, 5001 на 5001, и т.д. В правиле #2 указанный диапазон портов перенаправляется на 6000 порт. И в правиле #3 порт 5000 перенаправляется на 7000, 5001 на 7001, и т.д.
Перенаправление и Фильтрация Пакетов
Обратите внимание: Преобразованные пакеты проходят через фильтр и будут блокированы или пропущены, в зависимости от правил фильтрации.
Единственное исключение для этого правила это когда ключевое слово pass используется с rdr правилом. В этом случае перенаправленные пакеты будут пропущены сквозь работу фильтра: эти пакеты не будут оцениваться правилами фильтрации. Это сокращённый путь добавления фильтрующего правила pass для каждого правила перенаправления. Воспринимайте это как обыкновенное rdr правило (без ключевого слова pass) объединённое с фильтрующим правилом pass, с ключевым слово keep state. Однако, если вы хотите использовать более специфичные фильтрующие опции, такие, как synproxy, modulate state, и т.д. то вам необходимо отдельно использовать pass правило, потому что эти опции не работают в правилах перенаправления.
Также помните, что трансляция происходит до фильтрации, фильтр будет видеть уже транслированные пакеты с преобразованным ip адресом и портом, указанными в rdr правилах. Рассмотрим такой сценарий:
192.0.2.1 — хост в Интернет.
24.65.1.13 — внешний адрес OpenBSD роутера.
192.168.1.5 — внутренний адрес веб сервера.
Пакет до обработки rdr правилом:
Исходный адрес: 192.0.2.1
Исходный порт: 4028
Адрес назначения: 24.65.1.13
Порт назначения: 80
Пакет после обработки rdr правилом:
Исходный адрес: 192.0.2.1
Исходный порт: 4028
Адрес назначения: 192.168.1.5
Порт назначения: 8000
Фильтр увидит пакет в том виде, в котором он представлен после трансляции.
Пример заворота пакетов на прокси-сервер squid:
Если же некоторому IP-адресу нужно выходить в интернет, минуя прокси-сервер, то нужно добавить соответствующее правило Выше, чем правило для заворота:
Журналирование в пакетном фильтре осуществляется при помощи демона pflogd(8) слушающего сетевой интерфейс pflog0 и записывающего пакеты в журнальный файл /var/log/pflog в бинарном формате libpcap, который можно просматривать при помощи программы tcpdump(1) или wireshark(1), она же ethereal(1) (см. Раздел 6.11, «Демонстрация основных навыков работы с утилитой tcpdump(1)»). У программы tcpdump(1) есть специальные правила для работы с пакетным фильтром. Кроме того, программа tcpdump(1) позволяет просматривать журнал «на лету» если запустить её на прослушивание интерфейса pflog0. Для помещения в журнал, можно применять ключевое слово log (или log (all)) в правилах фильтрации.
Для журналирования пакета надо поместить ключевое слово log в правило фильтрации, nat или rdr. Заметьте, что в пактном фильтре нельзя создать правило только для журналирования пакета — должна присутствовать либо директива block либо pass.
Ключевому слову log можно передать следующие опции:
Помещает в журнал не только начальные пакеты, но вообще все пакеты соединения. Может применяться в правилах keep state.
Помещает в журнал UID и GID сокета, которому адресован пакет.
Начиная с OpenBSD 4.1 можно создавать несколько интерфейсов для журналирования и сообщения от разных правил посылать на разные интерфейсы. Ни в одной другой системе эта возможность пока не реализована.
Опции указываются в круглых скобках после ключевого слова log. Несколько опций можно указать через запятую или через пробел:
Это правило помещает в журнал все входящие пакеты, идущие на 22-й порт.
Журнальный файл записанный pflogd(8) имеет бинарный формат, его нельзя читать при помощи текстового редакора. Он предназначен для чтения утилитой tcpdump(1) (или другой программой скомпилированной с поддержкой библиотеки libpcap, например wireshark(1)).
Для просмотра журнального файла выполните команду
Для просмотра журнала в режиме реального времени:
Правила фильтрации в tcpdump(1) специально расширены для взаимодействия с pflogd(8).
В этом примере мы в режиме реального времени следим за входящими пакетами блокирующимися на интерфейсе wi0.
При написании статьи использовались материалы со следующих источников:
[pf] Файервол packet filter : 9 комментариев
Нет, не ошибся. «!» — означает отрицание. Из диапазона 192.0.2.0/24 исключается адрес 192.0.2.5
Не вводите в заблуждение, с 10 ветки pf поддерживает SMP.
Если вы посмотрите на дату поста, то поймёте, что тогда ещё не было SMP.
Почему 1е правило прекрасно работает а второе — никак?
$ext — внешний интерфейс, — наблица внешних IP для доступа?
Бьюсь над этой загадкой второй месяц. Отключаю PF — работают ОБА правила! Включаю — только первое.
Значит проброс прописан где-то ещё. При отключении файервола проброс не может работать, так как собственно нет механизма, который бы пробрасывал пакеты.