From: Антон Южанинов <http://citrin.ru>
Date: Mon, 24 Oct 2007 14:31:37 +0000 (UTC)
Subject: Использование ng_ipfw + ng_bpf для фильтрации по телу пакета во FreeBSD
Оригинал: http://citrin.ru/freebsd:ng_ipfw_ng_bpf
Использование ng_ipfw + ng_bpf для фильтрации по телу пакета
ipfw это удобный и гибкий пакетный фильтр, но без внешних средств
решение он может принимать только на основе данных в заголовке пакета
(ip и udp/tcp). Иногда этого недостаточно и нужно учитывать содержимое
пакета. Например у меня возникло желание зафильтровать DNS-запросы
MX-записей, которые в большом количестве делают зараженные спамерскими
троянами клиентские компьютеры, чем вызывают повышенную загрузку
DNS-сервера. В случае если клиентские компьютеры за NAT, почтовых
серверов на них быть не должно, и MX-записи им спрашивать не нужно.
Правильно ли так делать это отдельный вопрос, здесь будет рассмотрен
только технический аспект - как это сделать.
Для решения данной задачи есть как минимум три варианта:
* Из ipfw через divert-сокет отправлять пакеты процессу работающему
в userspace, который будет слушать divert-сокет и фильтровать
приходящие на него пакеты. Главный минус такого варианта - большие
накладные расходы и как следствие низкая производительность.
Другой минус - этот демон еще нужно написать. Хотя не исключено,
что кто то уже написал демон слушающий divert и фильтрующий пакеты
через bpf.
* ng_bpf - узел Netgraph использующий bpf для фильтрации пакетов
+ ng_bpf можно подключить непосредственно к ng_ether.
+ пакеты в ng_bpf можно отправлять из ipfw через ng_ipfw, при
выполнении определенного правила через.
Я выбрал последний вариант (ng_ipfw+ng_bpf), несмотря на то, что он
несколько сложнее (ниже напишу почему), чем ng_ether+ng_bpf. Во-первых
связка ng_ipfw+ng_bpf теоретически должна работать немного быстрее
(тесты не проводил), а во вторых из ipfw удобно рулить тем какие
пакеты отправлять на дальнейшую обработку в bpf, а какие нет.
bpf
Для начала нужно определиться по какому bpf-выражению будем
фильтровать пакеты. Исходная задача звучала так: DNS-запросы (бит QR
установлен в 1). Тип запроса - MX. Со вторым сложнее - в одном пакете
может быть запрос на несколько записей разных типов, а сами запросы
имеют переменную длину, и начинаются со строки заканчивающейся на 0
длина которой не указана в полях. Парсить такой запрос полностью в bpf
очень неудобно. Впрочем это и не нужно - запросы посылаемые спамботами
содержать запрос только на одну запись типа MX и можно просто
заглянуть в конец пакета - там будут поля Type (MX - 0x000f)и Class
(IN - 0 *0001). Если это проверять, то под правило будут подпадать
любые пакеты у которых в конце запрос на MX-запись.
При составлении выражения удобно пользоваться описанием формата
пакетов с сайта networksorcery.com (^1).
udp[10] & 0 *80 = 0 - проверяем, что 1-й бит, байта со смещением
10 (^2) выставлен в 1, т. е. то, что это запрос, а не ответ.
udp[udp[4:2]-4 : 4] = 0x000f0001 - последние 4 байта UDP пакета
0x000f0001. udp[4:2] это длинна UDP пакета в байтах (вместе с
заголовком).
В результате получается выражение:
udp dst port 53 and udp[10] & 0x80 = 0 and udp[udp[4:2]-4 : 4] = 0x000f0001
Но для ng_bpf нужно не выражение, а bpf-программа - набор
низкоуровневых инструкций, которые нужно выполнить для проверки
пакета. Чем то напоминает упрощенный и усеченный ассемблер.
В какие инструкции нужно преобразовать выражение зависит от того, что
подается на вход bpf-фильтра - IP пакеты (начиная с заголовка
IP-пакета без каких либо дополнительных заголовков), ethernet-фреймы
(т. е. заголовок канального уровня и далее IP пакет) или пакет какого
либо другого канального уровня.
В случае если ng_bpf подключен к ng_ether на его вход подаются
ethernet фреймы, и для получения bpf-кода можно воспользоваться
способом описанным в man ng_bpf - tcpdump -ddd и последующее
приведении в формат который нужен для netgraph с помощью awk.
В случае подключения ng_bpf к ng_ipfw на вход подаются IP пакеты и
такой способ не подойдет - в начале будет пара команд проверяющих поле
в заголовке ethernet-фрейма которого в нашем случае нету, а в
последующих командах будет использовано смещение на 14 байт больше чем
нужно.
Поэтому есть два способа - составить программу руками заглядывая в man
bpf и /usr/include/net/bpf.h (за основу можно взять вывод tcpdump
-ddd, но с нуля написать не сложнее чем вручную дизассемблировать
вывод tcpdump -ddd, изменить и потом снова перевести это в bpf-код).
Это процесс сильно напоминает программирование на ассемблере с
последующим переводом этого в машинные коды.
Или можно написать небольшую программку для компиляции выражения в
bpf-код проверки IP пакетов (в libpcap это тип DLT_RAW, т. е. "сырой"
IP пакет без каких либо дополнительных заголовков).
Cначала я составил bpf-код вручную, потом решил, что такой способ
хорошо только в учебных целях, поскольку отнимает много времени и для
повседневной работы не годится.
Потом написал маленькую программку которая это делает используя
libpcap:
Если у Вас получится программа, не 5 команд (bpf_prog_len=5), а 7,
значит libpcap собран с поддержкой IPv6 которая пока реализована не
очень хорошо. В результате для DLT_RAW создается фильтрующий код,
который кроме интересующих нас пакетов может поймать и лишние (это
происходит в libpcap до версии 0.9.5 включительно, а в 0.9.6 это
должно быть исправлено, но в любом случае если в сети используется
только IPv4, то лучше собирать libpcap без поддержки IPv6, чтобы
bpf-программа была короче и быстрее). Поэтому если у вас получилось 7
команд для udp, рекомендую пересобрать без поддержки IPv6:
echo 'NO_INET6=true' >> /etc/make.conf
cd /usr/src/lib/libpcap/
make clean
make
make install
Для интересующего нас выражения получается такая bpf-программа:
(первая команда в данном случае лишняя и её можно выкинуть, но можно
ли это делать зависит от того какая команда стоит за ней, в общем
случае проще оставлять программу как есть).
Netgraph
Для использования узлов типа ng_bpf и ng_ipfw нужно загрузить
соответствующие модули (если они не были включены в ядро):
:~# kldload ng_ipfw
:~# kldload ng_bpf
Чтобы после перезагрузки они загружались автоматически нужно добавить
в /boot/loader.conf
ng_ipfw_load="YES"
ng_bpf_load="YES"
При загрузке модуля ng_ipfw автоматически создается одни узел с именем
ipfw (дополнительные узлы типа ipfw создавать нельзя). Создаем узел
типа bpf и подключаем его к ipfw:
thisHook - хук, входящие пакеты с которого будут фильтроваться
заданной программой. ifMatch - хук куда будут отправляться пакеты для
которых выполнено условия фильтра. Если задать пустым, то пакеты будут
отбрасываться (что в данном случае и требовалось - фильтровать
определенные пакеты). ifNotMatch - все остальные пакеты отправляем
обратно в ipfw (^3).
Можно посмотреть, что программа действительно задана:
:~# ngctl msg dns_mx_q_filter: getprogram "main"
Теперь осталось интересующие нас пакеты отправить из ipfw через хук с
именем 1 в netgraph. Т. к. к хуку с именем 1 подключен узел типа
ng_bpf, то пакеты попадут к нему:
:~# ipfw add 123 netgraph 1 udp from 192.168.0.0/16 to me 53
Из 16333 пакетов отправленных правилом ipfw в ng_bpf, 9133 пакетов
были запросами на mx-записи.
Для конфигурации ng_ipfw+ng_bpf при загрузке можно положить скрипт
в /usr/local/etc/rc.d/ (расширение .sh нужно только во FreeBSD 4, 5.
Для 6-ки его лучше убрать).
комментарии по поводу этой заметки можно писать в ЖЖ
Ремарки:
1) первоисточник этой информации RFC, но в RFC эта информация
представлена не так наглядно и удобно
2) 8 байт, заголовок UDP и 2 байта идентификатор DNS запроса
3) что с ними будет дальше зависит от значения sysctl
net.inet.ip.fw.one_pass - либо будет разрешен, либо продолжится
проверка следующих правил
Скрипт для конфигурации ng_ipfw+ng_bpf при загрузке:
#!/bin/sh
# PROVIDE: ng_bpf
# REQUIRE: LOGIN abi
# BEFORE: securelevel
. /etc/rc.subr
name="ng_bpf"
# see http://citrin.ru/freebsd:ng_ipfw_ng_bpf for more info
#
# udp[10] & 0x80 = 0 - Query bit = 1
# udp[udp[4:2]-4 : 4] = 0x000f0001 - type MX and class IN (at the end of the packet)
#
# udp dst port 53 and udp[10] & 0x80 = 0 and udp[udp[4:2]-4 : 4] = 0x000f0001
bpf_prog="bpf_prog_len=17 bpf_prog=[ { code=48 jt=0 jf=0 k=9 } { code=21 jt=0 jf=14 k=17 }
{ code=40 jt=0 jf=0 k=6 } { code=69 jt=12 jf=0 k=8191 } { code=177 jt=0 jf=0 k=0 }
{ code=72 jt=0 jf=0 k=2 } { code=21 jt=0 jf=9 k=53 } { code=80 jt=0 jf=0 k=10 }
{ code=69 jt=7 jf=0 k=128 } { code=72 jt=0 jf=0 k=4 } { code=20 jt=0 jf=0 k=4 }
{ code=12 jt=0 jf=0 k=0 } { code=7 jt=0 jf=0 k=0 } { code=64 jt=0 jf=0 k=0 }
{ code=21 jt=0 jf=1 k=983041 } { code=6 jt=0 jf=0 k=65535 } { code=6 jt=0 jf=0 k=0 } ]"
ngctl="/usr/sbin/ngctl"
start_cmd="start_cmd"
# stop not implemented
stop_cmd=":"
extra_commands="stats"
stats_cmd="getstats_cmd"
start_cmd()
{
# modules must be already loaded (via /boot/loader.conf)
debug "create ng_bpf node and connect to ipfw"
$ngctl mkpeer ipfw: bpf 1 main
$ngctl name ipfw:1 dns_mx_q_filter
debug "set bpf program"
$ngctl msg dns_mx_q_filter: setprogram { thisHook="main" ifMatch="" ifNotMatch="main" $bpf_prog }
}