Vadim Goncharov (nuclight) wrote,
Vadim Goncharov
nuclight

Category:

Как работает tcpdump: ассемблер BPF; фильтрация с ng_bpf на FreeBSD

Этот пост пригодится вам не только на FreeBSD, но и на Linux и любой другой системе с BPF (для Windows есть вот такое) в случае, когда вы хотите написать приложение, отбирающее напрямую с линии пакеты по некоторому критерию, как tcpdump (ну скажем, хотите проконтролировать ARP в вашей сети по типу приложения ipguard, или еще что). Здесь идет более подробная версия куска моей презентации на RootConf 2009 (по ссылке доступны слайды и видео), где также рассматривался и упоминаемый далее Netgraph.

Итак, в 80-е годы, когда поддержка Интернета в юниксах родилась и активно росла вместе с самим Интернетом, потребовались и средства для диагностики, а также фильтрации пакетов. Это еще не были файрволы, пока речь шла о просто отборе. Нужен был способ задавать произвольные заранее при компиляции неизвестные критерии, так родился tcpdump. Как дать способ задать что угодно? Только программированием. И в ядре ОС была создана специальная своего рода виртуальная машина со своим ассемблером (реализацию машины можно посмотреть в линуксе в /usr/src/linux/net/core/filter.c, в BSD в /usr/src/sys/net/bpf_filter.c), в которую можно загружать инструкции из приложения пользователя. Ядро будет выполнять их для каждого пакета, если программа сказала, что да — отдаст пакет приложению. Что представляет собой этот "процессор" ?

  • Разрядность: слово 32 бита, половина слова 16 бит. Сетевой порядок байт.

  • Два регистра: аккумулятор A и индексный регистр X

  • 16 слов памяти: массив M[]

  • Опкоды (opcode, код операции, т. е. инструкция) одинаковой длины (8 байт), максимум 512 штук


Более подробно машина и имеющиеся инструкции описаны в bpf(4) в разделе FILTER MACHINE. Лимиты (количество слов памяти и максимальное число инструкций), вместе со значениями опкодов самих команд, задаются в /usr/include/net/bpf.h. Там же заданы значения и другой интересной штуки, которая называется DLT и имеет значение (применяется) совсем не только в tcpdump. Это Data Link Type, тип канала связи. Их более сотни [UPD: официально они перечислены на http://www.tcpdump.org/linktypes.html], для примера две штуки:

  • #define DLT_EN10MB 1 /* Ethernet 10Mb */

  • #define DLT_RAW 12 /* raw IP */


Первый говорит, что перед нами обычный канал Ethernet с 14-байтным заголовком, второй — что никакого L2-заголовка нет, сразу идёт IP-пакет. При запуске tcpdump смотрит на это значение, чтобы сгенерировать правильную программу — в инструкциях указаны просто смещения, сырые байты, ничего о структуре пакетов BPF-машина не знает. Рассмотрим пример — у tcpdump есть отладочный ключ -d, который вместо работы выводит сгенерированную им программу, которая была бы отправлена в ядро, причем выводит в относительно человекочитаемом виде, похожем на настоящий ассемблер (в отличие от циферок и структур Си в man bpf); делает это функция из файла bpf_image.c:
# tcpdump -d -s 123 src host 1.2.3.4
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 4
(002) ld       [26]
(003) jeq      #0x1020304       jt 8    jf 9
(004) jeq      #0x806           jt 6    jf 5
(005) jeq      #0x8035          jt 6    jf 9
(006) ld       [28]
(007) jeq      #0x1020304       jt 8    jf 9
(008) ret      #123
(009) ret      #0

Здесь видно, что запущен он был на сетевом интерфейсе Ethernet — нулевая инструкция загружает в аккумулятор половину (half) слова, 2 байта, по смещению 12 — в Ethernet-заголовке лежит тип пакета. Дальше инструкция под номером 1 сравнивает аккумулятор со значением 0x800 — это IP-пакет или нет. И делает условный переход (jump if equal) — если истинно (jump if true), то переход на инструкцию с номером 2, если ложно (jump if false) — то на инструкцию с номером 4. В инструкциях 4 и 5 видны аналогичные проверки, только здесь уже на типы пакетов для протоколов ARP и RARP. Если они выполняются, то с соответствующего для каждого протокола смещения в пакете загружается слово и сравнивается с заданным адресом хоста. Здесь одинаковым цветом обозначено то, что tcpdump при компиляции из входных параметров преобразует в код — в инструкциях 8 и 9 виден возврат из машины BPF. Возвращает программа либо код 0 (пакет не совпал), либо число байт пакета, которое ядро должно передать приложению в юзерлэнд (в данном случае параметр -s у tcpdump).
Необходимо отметить, что код BPF выполняется в ядре, и если он некорректно написан, например, если программа будет крутиться в бесконечном цикле, то компьютер по сути зависнет (по крайней мере сетевой стек). Поэтому циклов в BPF нет — все условные переходы разрешены только вперед, число инструкций ограничено, так что программа гарантированно выполнится за конечное время. К сожалению, это ограничивает допустимую сложность программ (например, не получится эмулировать поиск подстроки в произвольном месте по типу iptables -m string).
Приведем еще один пример, с комментариями внутри — как для достаточно просто выглядящего выражения tcpdump генерирует довольно много проверок, которые должны быть учтены в реальном пакете:
$ tcpdump -d host 195.208.174.177 and not port 22

Здесь и проверка на сразу несколько протоколов (какой из них имел в виду админ?), и проверка на то, что, если пакет фрагментирован, здесь точно есть заголовок протокола уровня выше. А в инструкции 12 видно команду, которая в man bpf названа BPF_MSH — это специальный хак, который умножает младшую половину указанного байта на 4 и загружает в X (то есть, получает длину заголовка IP или TCP). На самом деле, ограничения на объем поста в ЖЖ не позволяют привести здесь листинг, так что поглядите в него самостоятельно.

Кто есть кто


Выше везде говорилось, что программу из выражения компилирует tcpdump, но это не совсем так. На самом деле это делает библиотека libpcap. Проект tcpdump много лет назад разделился на две составляющие — собственно tcpdump и библиотеку libpcap, которая умеет принимать с командной строки выражение, компилировать его в инструкции BPF, получать из ядра пакеты, и отдавать их основному приложению (дальше его проблемы), а еще читать-писать файлы с этими пакетами. Библиотеку использует несколько десятков различных программ, и её формат файлов стал де-факто стандартом для обмена дампами пакетов. Так что и выражение для отбора пакетов вы можете задавать в самых различных программах для анализа трафика, и сохранять в одной, загружать в другой, например, сохраненный на далеком сервере pcap-файл из tcpdump можно загрузить локально в графическом Wireshark (Ethereal).

Если же вы вдруг не хотите в своем проекте использовать libpcap (ну может выражение жестко задано и менять нельзя, зачем тащить лишнее), а вручную составлять программу не хочется, то tcpdump со своими отладочными ключами придет на помощь и тут: tcpdump -dd выдаст скомпилированное выражение в виде готовых Си-структур, а tcpdump -ddd выдаст просто набор цифр (сначала строку с количеством инструкций, потом по 4 числа на строку для каждой инструкции), который можно включить куда-нибудь еще, во что-нибудь менее распространенное. Правда, тому, кто потом будет эти циферки читать и расшифровывать, будет неудобно — декомпилятора-то не предусмотрено. Но перевести обратно в ассемблер BPF и понять, что здесь, всё-таки можно. Возьмём пример — код с темы nag.ru, который фильтрует пакеты uTP, и посмотрим на каждую его инструкцию (в формате ng_bpf) более внимательно, с комментариями, что оно делает:
bpf_prog_len=12 bpf_prog=[
{ code=48 jt=0 jf=0 k=0 }          BPF_LD+BPF_B+BPF_ABS    A <- P[k:1]              ; загрузить в A нулевой байт
{ code=84 jt=0 jf=0 k=240 }        BPF_ALU+BPF_AND+BPF_K   A <- A & k               ; получить его старшую половину
{ code=21 jt=0 jf=8 k=64 }         BPF_JMP+BPF_JEQ+BPF_K   pc += (A == k) ? jt : jf ; это 0x40, т.е. IPv4 ?
{ code=48 jt=0 jf=0 k=9 }          BPF_LD+BPF_B+BPF_ABS    A <- P[k:1]              ; загрузить байт IP-заголовка со смещ. 9
{ code=21 jt=0 jf=6 k=17 }         BPF_JMP+BPF_JEQ+BPF_K   pc += (A == k) ? jt : jf ; это протокол 17, т.е. UDP ?
{ code=40 jt=0 jf=0 k=6 }          BPF_LD+BPF_H+BPF_ABS    A <- P[k:2]              ; загрузить 16 бит по смещению 6
{ code=69 jt=4 jf=0 k=8191 }       BPF_JMP+BPF_JSET+BPF_K  pc += (A & k) ? jt : jf  ; это первый фрагмент или нет?
{ code=177 jt=0 jf=0 k=0 }         BPF_LDX+BPF_B+BPF_MSH   X <- 4*(P[k:1]&0xf)      ; получить в X длину IP-заголовка
{ code=64 jt=0 jf=0 k=20 }         BPF_LD+BPF_W+BPF_IND    A <- P[X+k:4]            ; загрузить 32 бит с 20-го байта после IP
{ code=21 jt=0 jf=1 k=2147483647 } BPF_JMP+BPF_JEQ+BPF_K   pc += (A == k) ? jt : jf ; там лежало 0x7fffffff ?
{ code=6 jt=0 jf=0 k=65535 }       BPF_RET+BPF_K           accept k bytes           ; выход, пакет совпал, -s 65535
{ code=6 jt=0 jf=0 k=0 } ]         BPF_RET+BPF_K           accept k bytes           ; выход с 0 байт - non-match

Сложно? На самом деле ничего сложного, просто муторно. Берем число, файл bpf.h, остаток от деления его на 8, получаем класс инструкции (до первого плюса). Оставшуюся часть расшифровываем согласно классу, там всего-то еще одно или два числа. Потом выписываем из man bpf соответствие для суммы, как можно заметить, оно в примере прямо скопировано оттуда, расшифровка, что делает операция. Затем комментируем, затем смотрим на всё в сборе... и видим, что получено оно было из команды tcpdump -ddd 'udp[20:4] = 0x7fffffff' (при типе DLT_RAW, см. ниже) — выполнение этой команды показывает идентичные инструкции.

Еще к этой теме следует заметить, что, поскольку компилирует libpcap, то, хотя вы и видите в дампе что-нибудь типа 'DNS внутри IPIP-туннеля', эту расшифровку содержимого делает уже tcpdump по полному пакету, в него много читалок форматов встроено. Но libpcap об этом ничего не знает, и просто задать что-нибудь типа "а покажи-ка мне трафик к такому-то хосту внутри туннеля", увы, нельзя.

Но tcpdump не умеет блокировать пакеты


Да, не умеет. Но во FreeBSD есть netgraph, универсальный сетевой фреймворк, в рамках которого есть средства, которые могут. Что такое нетграф и чем он так хорош — отдельная тема, не вписывающаяся в объемы поста. Кое-что есть в презентации. В двух словах, нетграф — это такая хрень, которая состоит из узлов (node), которые есть модули, подобно тому, как командная строка с пайпами в юниксах состоит из отдельных программ-фильтров. И вот собранная из этих "строительных блоков" схема позволяет делать с пакетами всякие хитрозавернутые вещи — netflow, там, посчитать, или занатить что-то, или зашейпить. А еще я в неё ем на нём работает mpd. Ну и среди имеющихся модулей есть ng_bpf(4) — как вы уже догадались, экземпляр BPF-машины. С точки зрения использования в ipfw для данной цели можно считать, что пакет просто уходит в netgraph, потом возвращается обратно — если вы использовали divert natd, это выглядит точно так же, только ключевое слово другое.

Возникает, правда, вопрос: каким образом этот самый ng_bpf обучить делать нужную работу. После примера выше (а он был именно для ng_bpf) и правда — вручную составлять BPF-инструкции и считать циферки (а он принимает именно их) как-то не хочется. На этот случай в tcpdump можно увеличить число ключей дебага, и он выведет вместо ассемблера — циферки, так сказать, "машинный код". Преобразовать один текст в другой — уже дело техники, на этот случай в man bpf приведен скрипт, в котором на входе выражение для tcpdump, на выходе — программирование ноды. Надо, кстати, отметить, что раньше там создавался временный файл со скриптом на awk, в котором была конвертация в числа (%d) и обратно, из-за чего оно на ряде значений выдавало неверные результаты. Я для себя (и в примерах ниже) просто исправил это на %s, год назад man-страницу поправили, теперь там не awk, а чуть более медленная версия прямо на шелле без временных файлов. Как говорится, выбирайте сами.

Перед началом работы, однако, остаются кое-какие подводные грабли. Заключаются они в том, что tcpdump генерирует программу для того интерфейса и типа DLT, который ему указали (обычно же первый Ethernet в системе). Если ng_bpf будет использваться на ng_ether, это нормально, там DLT_EN10MB и используется, программа будет корректной. Но при вызове из ipfw никаких L2 заголовков нет, чистый IP, то есть нужен DLT_RAW. А где его взять, таких интерфейсов в системе нет?.. По счастью, tcpdump умеет читать и писать дампы пакетов в pcap-файлы, в этих файлах указывается тип DLT. Если раздобыть такой файл, то программу можно получить на нём. И раздобыть его можно — в системе с 7 ветки есть утилита ipfwpcap(8), которая умеет вешаться на divert-сокет (как natd) и писать пакеты в понимаемый tcpdump формат (это аналог pflogd(8)), с нужным нам DLT. На самом деле, если лень извращаться с ipfwpcap, файл dltraw.pcap (там только лишь 24 байта заголовка для определения типа линка), можно получить так:
echo '1MOyoQIABAAAAAAAAAAAAP//AABlAAAA' | uudecode -mr > dltraw.pcap

Таким образом можно делать разные прикольные вещи — например, фильтровать нешифрованные TCP-соединения торрентов или вот товарищ написал фильтр для блокировки передачи файлов по ICQ; в ng_tag(4) есть пример блокировки DirectConnect.
Nota bene: Раньше я уже писал, что в свое время коллега Citrin (ospf_ripe) делился опытом фильтрации спамерских DNS-запросов на MX-записи — и для решения проблемы DLT_RAW он написал использующую libpcap программу на Си — на http://citrin.ru/freebsd:ng_ipfw_ng_bpf приведён её исходник. Эта программа берет выражение для tcpdump и сразу выдает код для ng_bpf в расчете на то, что он будет подключен напрямую к ng_ipfw, остается только добавить другие команды для создания ноды. В упоминавшейся выше теме на nag.ru (и разобранном примере) был показан именно её вывод, без оригинального выражения. Вы можете сразу использовать его программу для упрощения своих скриптов, если нужен именно DLT_RAW, во всех остальных же случаях (например, подключение к ng_ether, бридж и т.п.) или если хочется обойтись лишь встроенными в базовую систему средствами (без лишнего чужого кода на Си), потребуется использовать (и понимать) более сложные скрипты для tcpdump — именно за этим и написан сей пост.

Пример из жизни


В ноябре 2008 года столкнулись мы в нашей сети с пренеприятной ситуацией — резко увеличилось потребление внешнего трафика. Томск, как известно, большая локалка, тарифы за его пределы — совсем другие, помегабайтные. Оказалось, что дело происходит так: юзер устанавливает PPTP-соединение с каким-нибудь местным провайдером, у которого по VPN соотношение скорость/тарифы другое. Винда, каким-то немыслимым образом роутит часть пакетов с БЕЛЫМ адресом сетевухи в ТУННЕЛЬ. И наоборот, кстати, тоже. Причем как, так и осталось непонятным (маршруты-то смотрят совсем не туда, если по ним выбор адреса делать). Провайдер, засранец, на своем VPN-концентраторе нифига не фильтрует чужие src-адреса (а ну-ка, поднимите руки, кто у себя со спуфингом борется?). В итоге пакет с не тем адресом улетает во "внешку" и возвращается на другой интерфейс юзера, по совсем другому тарифу. Чтобы сделать картину еще более веселой, эти пакеты были зафильтрованы — а винда всё равно шлет, такое впечатление, что у неё стоит рандом в N% пакетов направлять не туда. И такое у большинства юзеров сети — по всем уже не пройдешься выяснять, да... а проблема выливается в реальные деньги. Было предположение, что так может гадить uTorrent — он чего-то там с сетевым стеком любит делать, как минимум в изменении TTL замечен. Так что скорее всего надо еще и блокировать DHT, который он тоже любит слать куда попало в мир.

То есть, надо отфильтровать наши src-адреса внутри GRE-туннеля. Стандартными средствами никак. На помощь приходит netgraph. Остается "малость" — составить выражение, чтоб оно ловило такие пакеты внутри туннеля, ибо вручную наверняка будет очень сложно. Тут и на нормальном-то языке, как увидим ниже, получается нечто длинное. Но сначала — надо понять, как вообще ловить.

Итак, читаем документацию на протоколы, RFC 1701, RFC 1702, RFC 2637, пачка RFC по PPP... схемы из RFC на IPv4 и так в голове всё время из "TCP/IP Illustrated" Стивенса... Мы же админы, нам за это деньги платят, чтобы мы в байтиках протоколов разбирались. Итак, оказывается, что:
  • сначала идет заголовок IP, длина из-за опций может быть переменной

  • потом идёт заголовок GRE, причем в модификации для PPTP там свои особенности, длина тоже может быть разной

  • затем идет инкапсуляция PPP для IP, причем она может быть в нескольких вариантах (в реальных пакетах действительно разные видел)

  • наконец, затем идет заголовок внутреннего IP-пакета, тоже может быть переменной длины из-за опций, и только в нём искать DHT в UDP-пакетах

Язык выражений tcpdump/libpcap позволяет брать байты с нужных смещений, делать с ними вычисления, подставлять в другие выражения — но это всё в целом одно выражение, а не программа с переменными, то есть какие-то части вычислений нужно всё время повторять. Похоже на функциональное программирование, только без функций. Так что будем путаться в скобочках, и надо уже какое-нибудь средство, чтобы облегчить написание выражения для tcpdump!

По счастью, в системе есть встроенная утилита cpp(1) — препроцессор языка Си, который обрабатывает директивы типа #include и #define (и некоторые другие). То есть, умеет простые и параметризованные подстановки, его когда-то часто использовали не по назначению, а для генерации всякой дребедени типа web-страниц. Воспользуемся им и мы — это не язык Си, всё гораздо проще. Причем он умеет подстановки с параметрами — вот, например, если есть строчка:
#define IPHDRLEN(firstbyte) ((ip[firstbyte]&0xf)<<2)
- то при использовании в тексте вместо IPHDRLEN(0) будет подставлено ((ip[0]&0xf), а вместо IPHDRLEN(20) — уже ((ip[20]&0xf). Обратите внимание, что вторую часть, которая есть то, что подставляется, мы дополнительно берем в еще одни круглые скобки. На всякий случай, как советуют учебники по Си — мало ли какой приоритет операций будет в том месте, куда подставляем. А машина лишние скобки переварит, не мы же будем это читать. Итак, начинаем последовательно упрощать задачу, а затем строить с самых малых "процедур" — нам сначала надо проверить, что пакет представляет собой действительно GRE от PPTP, а не что-нибудь другое:
#define GRESTART IPHDRLEN(0)
#define VALID_PPTP_GRE ((ip[GRESTART:4] & 0xff7fffff) = 0x3001880b)

Здесь проверяются именно те сигнатуры, которые там будут, с учетом того, что один бит — флаг. Который показывает, кстати, есть ли после GRE-заголовка еще 4 байта — а их ведь тоже надо учесть. Но в языке tcpdump/libpcap нет условных операторов. Что же делать, что же делать? Мы поступим умно и значение этого бита, замаскировав всё остальное, применим для вычисления длины:
#define GRE_DATA_START (GRESTART + ((ip[GRESTART+1] & 0x80) >> 5) + 12)

Следуя хорошему стилю, определим и константы для наших адресов, которые ловить (чтоб если что, не менять их по всему файлу). Напишем макросы (эти подстановки называются в Си именно так) для определения, UDP ли внутри, DHT ли внутри этого UDP, или это не-UDP пакет, но с нашими src-адресами. Наконец, сведём всё это воедино в итоговом выражении и запишем в файл с именем tcpdump-gre-addr-cpp всё получившееся:
#define IPHDRLEN(firstbyte) ((ip[firstbyte]&0xf)<<2)
#define GRESTART IPHDRLEN(0)
/* Check that is GREv1 with seq num and proto set per RFC 2637 */
#define VALID_PPTP_GRE ((ip[GRESTART:4] & 0xff7fffff) = 0x3001880b)
/* ACK is optional 4 bytes to previous 12 */
#define GRE_DATA_START (GRESTART + ((ip[GRESTART+1] & 0x80) >> 5) + 12)
/* Actual IP subnet/Mask to find in the src IP of inner IP datagram */
#define SUBNET  0x52754000      /* 82.117.64.0 */
#define MASK    0xffffff00      /* 255.255.255.0 */
#define INNER_SRC_EQ_SUBNET(ppp_hdr_len)        (ip[(GRE_DATA_START+ppp_hdr_len+12):4] & MASK = SUBNET)
/* Torrent DHT UDP payload begins with "d1:?d2:id20:", we'll skip 4 bytes and check other 8 */
#define IS_TORRENT_DHT(udp_hdr_start)   ((ip[(udp_hdr_start+12):4]=0x64323a69)/*and (ip[(udp_hdr_start+16):4]=0x6432303a)*/)
/* Check inner IP has UDP payload (proto 17) then calculate offset and pass it to DHT macro */
#define INNER_IS_UDP(ppp_hdr_len)       (ip[GRE_DATA_START+ppp_hdr_len+9]=17)
#define INNER_UDP_OFFSET(ppp_hdr_len)   (GRE_DATA_START+ppp_hdr_len+IPHDRLEN(GRE_DATA_START+ppp_hdr_len))
#define INNER_IS_DHT(ppp_hdr_len)       (INNER_IS_UDP(ppp_hdr_len) and IS_TORRENT_DHT(INNER_UDP_OFFSET(ppp_hdr_len)))

/*
 * Finally, expression: sort by most frequent pattern first.
 * We check four possible PPP headers corresponding to IP, then
 * pass length of matched PPP header to checking macros.
 */
proto gre and VALID_PPTP_GRE and (
        (
                (ip[GRE_DATA_START]=0x21) and (INNER_SRC_EQ_SUBNET(1) or INNER_IS_DHT(1))
        ) or (
                (ip[GRE_DATA_START:2]=0xff03) and (ip[GRE_DATA_START+2]=0x21) and (INNER_SRC_EQ_SUBNET(3) or INNER_IS_DHT(3))
        ) or (
                (ip[GRE_DATA_START:4]=0xff030021) and (INNER_SRC_EQ_SUBNET(4) or INNER_IS_DHT(4))
        ) or (
                (ip[GRE_DATA_START:2]=0x0021) and (INNER_SRC_EQ_SUBNET(2) or INNER_IS_DHT(2))
        )
)

Вполне понятный код в итоговом выражении. Теперь посмотрим, во что это преобразует препроцессор, что было бы, если бы нам пришлось писать это выражение вручную в командной строке:
$ cpp -P tcpdump-gre-addr-cpp
proto gre and ((ip[((ip[0]&0xf)<<2):4] & 0xff7fffff) = 0x3001880b) and (
 (
  (ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)]=0x21) and ((ip[((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+1 +12):4] & 0xffffff00 = 0x52754000) or ((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+1 +9]=17) and ((ip[(((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+1 +((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+1]&0xf)<<2))+12):4]=0x64323a69) )))
 ) or (
  (ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12):2]=0xff03) and (ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+2]=0x21) and ((ip[((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+3 +12):4] & 0xffffff00 = 0x52754000) or ((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+3 +9]=17) and ((ip[(((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+3 +((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+3]&0xf)<<2))+12):4]=0x64323a69) )))
 ) or (
  (ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12):4]=0xff030021) and ((ip[((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+4 +12):4] & 0xffffff00 = 0x52754000) or ((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+4 +9]=17) and ((ip[(((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+4 +((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+4]&0xf)<<2))+12):4]=0x64323a69) )))
 ) or (
  (ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12):2]=0x0021) and ((ip[((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+2 +12):4] & 0xffffff00 = 0x52754000) or ((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+2 +9]=17) and ((ip[(((((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+2 +((ip[(((ip[0]&0xf)<<2) + ((ip[((ip[0]&0xf)<<2)+1] & 0x80) >> 5) + 12)+2]&0xf)<<2))+12):4]=0x64323a69) )))
 )
)

Совершенно нечитаемо, правда?
Но машина железная, она переварит. Итак, создаем файл gre-addr-block.sh, который создаст нам из всего этого ноду и добавит правила в файрвол:
#!/bin/sh

PATTERN=`cpp -P tcpdump-gre-addr-cpp`
NODENAME="greavtaddrdeny"
NODEPATH="$NODENAME:"
INHOOK="ipfw"
MATCHHOOK="matched"
NOTMATCHHOOK="ipfw"

cat > /tmp/bpf.awk << xxENDxx
{
  if (!init) {
    printf "bpf_prog_len=%d bpf_prog=[", \$1;
    init=1;
  } else {
    printf " { code=%d jt=%d jf=%d k=%s }", \$1, \$2, \$3, \$4;
  }
}
END {
  print " ]"
}
xxENDxx

BPFPROG=`tcpdump -s 8192 -r dltraw.pcap -ddd ${PATTERN} | awk -f /tmp/bpf.awk`

ngctl shutdown ${NODEPATH} > /dev/null 2>&1
ngctl mkpeer ipfw: bpf 190 ${INHOOK}
ngctl name ipfw:190 $NODENAME

ngctl msg ${NODEPATH} setprogram { thisHook=\"${INHOOK}\" \
  ifMatch=\"${MATCHHOOK}\" \
  ifNotMatch=\"${NOTMATCHHOOK}\" \
  ${BPFPROG} }

ipfw add 4492 netgraph 190 gre from 82.117.64.0/24 to any iplen 60-1500 #out xmit em1


Всё это хозяйство было опробовано и успешно работало на FreeBSD 6.4, на роутере с 100 Mbit и 15 kpps в каждую сторону. На самом деле (ограничения на объем поста в ЖЖ опять-таки не позволили рассказать подробнее), последовательность была несколько не такой: сначала было испытано выражение без проверок INNER_IS_DHT(), затем они были добавлены, и оказалось, что tcpdump генерит для этого очень длинную программу, которая перестала влезать в 512 инструкций. Оптимизатор у него не настолько умный, чтобы повторяющиеся вычисления засунуть в память BPF-машины, вместо повторения. Поэтому кусок с проверкой на валидность GRE был закомментирован:
/* proto gre and VALID_PPTP_GRE and /* (
Тогда влезло. Клиентов, у которых были бы чистые GRE-туннели, не PPTP, не нашлось — во всяком случае, никто не жаловался :)

Tags: freebsd, ipfw, l7, netgraph, админское, объяснение, сети
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 15 comments