Ключевые слова:freebsd, ipfw, l7, netgraph, bpf, tcpdump, firewall, (найти похожие документы)
From: Vadim Goncharov <vadimnuclight@tpu.ru.>
Date: Mon, 10 Mar 2010 17:02:14 +0000 (UTC)
Subject: Построение BPF-правил для фильтрации в ng_bpf во FreeBSD
Оригинал: http://nuclight.livejournal.com/124989.htmlhttp://nuclight.livejournal.com/125372.html
Как работает 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, тип канала связи. Их более сотни,
для примера две штуки:
* #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, там,
посчитать, или занатить что-то, или зашейпить. А еще [DEL: я в неё ем
:DEL] на нём работает 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.
Пример из жизни
В ноябре 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,
не нашлось - во всяком случае, никто не жаловался :)
Фильтрация uTP (Torrent UDP) внутри PPTP GRE
Краткое содержание: как отфильтровать uTP на FreeBSD в теме на Наге уже
сказали, однако для случая пакетов внутри туннеля PPTP GRE решения не
было, о чем здесь и будет рассказано, но сначала - предыстория.
У вас вдруг стал хуже работать Интернет?..
В конце 2008 года разработчики uTorrent собрались заменить транспортный
протокол с TCP на UDP, реализовав поверх него свою прослойку под
названием чTP - дескать, так будет эффективнее. Тогда же исследователь
по имени Ричард Беннет заявил, что возникнет большая проболема для всего
Интернета - UDP менее 2% во всём трафике, он используется для
приложений реального времени (игры, телефония) и для критически важного
DNS, в общем, пострадают все, из-за отсутствия в нём нормального
congestion control (обработки перегрузок сети). Разработчики на это
ответили, что они сделают congestion control еще лучше, чем в TCP, и
торрент станет даже меньше забивать каналы и мешать другим, чем сейчас.
И включили по умолчанию новый протокол uTP в бета-версиях uTorrent 1.8
(правда, сначала только на прием). Время шло, обещанной проблемы всея
Интернета не было...
До этого февраля. Буквально в понедельник админы бывшего СССР начали
спрашивать друг друга, у всех ли отмечено сильное возрастание нагрузки
за последние дни. Таки да, оказалось у многих. Выяснилось, что месяц
назад вышел uTorrent 2.0, у кучи народа вылезло окошко с предложением
обновиться, и с начала февраля пошел рост PPS (нагрузки по пакетам в
секунду), причем обратите внимание, не трафика. Много где оборудование
к такому неожиданному повороту сюжета было не готово, и проблемы работы
Интернета ощутили на себе все, не только "качки". Более того, не только
оборудование провайдеров - у многих возникли проблемы с домашними роутерами и
ADSL-модемами (вот пост на Хабре на тему), хотя опять же человек
может не связать обновление у себя uTorrent и начать обвинять
провайдера.
Вот это обсуждение на nag.ru: http://forum.nag.ru/forum/index.php?showtopic=55025&st=160&p=478584
В этой теме было много интересного, например, такая проблема не только
у нас, в Амстердаме тоже отмечают увеличение PPS за этот месяц. На
этот топик понабежали "хомячки" после обсуждений на том же Хабре и
даже на закрытом torrents.ru начали подозревать, что это связано с
происками провайдеров. Там было найдено и решение - блокировать пакеты
установления соединения поверх UDP по сигнатуре, она оказалась
достаточно простая, приведены строки конфигов для разного железа. Надо
отметить, что провайдеры имеют на это полное моральное право, но есть и юридическое
обоснование, в теме приведена выдержка из Постановления РФ по такому
случаю. Кто-то даже предлагал привлечь разработчиков uTorrent по ст.
272-273 УК РФ (благо среди них есть русские) - за фактический DDOS на
оборудование.
Обнаружилось и много других интересных вещей, теперь по технической
части. Во-первых, спецификация uTP на
http://www.bittorrent.org/beps/bep_0029.html оказалась не той, что
реально применяется сейчас - и разработчики uTorrent сказали кому-то в
IRC, что сейчас ловят по одному из начальных значений, которое в
будущем будет меняться, то есть приведенные решения в будущем
перестанут работать. А учитывая, что количество обновляющихся на новую
версию юзеров всё растёт... думаю, понимание принципов его блокировки
еще пригодится, чтобы адаптировать способы.
Разработчиков спрашивали на форумах еще в прошлом году и критиковали за
ряд решений. Более того, спецификация еще и не была доступна для
публичного review, как это принято в нормальном случае разработки
стандартов Internet. На http://forum.bittorrent.org/viewtopic.php?id=162
заметили, что реальные пакеты uTP не соответствуют
спецификации, но ответа разработчиков пока нет. Но - они ж опробовали в
локалке, и всё работало замечательно, хехе. К чему нам опыт 30-летней
разработки TCP, который разрабатывался, заметим, учеными в
университетах?.. Которые его отлаживали и исправляли до действительно
массового внедрения на реальных ошибках - первый опыт перегрузки
(meltdown) Сети был в конце 80-х. Суть введенных тогда механизмов
congestion control (контроля перегрузок) - при отправке данных TCP-стек
вашего компьютера постепенно "разгоняет" поток, до тех пор, пока
принимающая сторона не сообщит, что часть пакетов не дошла. Тогда
делается вывод, что канал забит полностью, надо немного понизить
скорость (и такие проверки делаются постоянно, потому что маршруты в
Интернете могут в любой момент измениться, и в канале могут еще
находиться пакеты других пользователей). Со временем оно обросло
сложной математикой (чтобы точнее и быстрее сходилось), оборудование у
провайдеров также рассчитано на такое поведение TCP, все методы
регулировки полосы и обеспечения качества связи это учитывают. А на UDP
отклика от другой стороны нет, можно послать слишком много пакетов и
"забить" канал (причем не только себе), этот отклик придется делать
вручную (фактически изобретая TCP заново).
У этого протокола есть ряд принципиальных архитектурных проблем,
которые ведут к большому числу пакетов. Это мешает даже его
непосредственной цели - улучшить скачивание. Например, на
http://forum.utorrent.com/viewtopic.php?id=69592 отметили, что "uTP
used 13130 packets, and TCP only 8499 (uTP used 55% more packets than
TCP!)" - а это значит, что для файла того же размера трафика на TCP
будет меньше.
Одна из ключевых проблем - поверх UDP эмулируется такой же потоковый
протокол, как TCP, то есть, пакеты приложению должны приходить по
порядку. Фактически, uTP - это во многом изобретение велосипеда,
справедливо указывали, что можно было бы просто внедрить нужные
механизмы в TCP (правда, это потребует апгрейд всех операционных
систем, да). Но и без этого, протоколу передачи файлов, а особенно
такому, как BitTorrent, совершенно неважно, в каком порядке передаются
блоки файла, можно было бы в UDP это соптимизировать. Им
предлагались решения получше. Я бы еще добавил туда DCCP - он,
например, хотя бы ECN поддерживает, в отличие от.
Другая из проблем, увеличивающих PPS - это репакетизация и MTU.
Максимальный размер пакета на Ethernet 1500 байт, в случае PPPoE
(например, ADSL) - он будет уже 1492 байта. Если целый пакет "не
пролезает" - без проблем, TCP нумерует байты, он может разбить их на
два пакета так, что будет использован максимальный доступный MTU. Но в
uTP нумеруются не байты, а пакеты. Это значит, что если был потерян
пакет, то его нельзя разбить, надо перепослать его же целиком,
иначе будет ошибка в нумерации. А если он не пролезает из-за MTU - его
остается только фрагментировать на том роутере, где уменьшается. То
есть, после такой точки в сети (а это типичный случай на том же ADSL,
от компа до модема 1500, от модема до провайдера 1492) пойдет в два
раза больше пакетов.
У TCP на это дело есть худо-бедно, но работающий механизм PMTUd,
обнаруживает и подстраивается. Разработчики uTP решили, что можно им
воспользоваться - если в ответ пришлют ICMP need-fragment. Но они не
учли, что много где неграмотные админы блокируют на файрволах ICMP
целиком, чем ломают этот механизм. Если у вас когда-нибудь была
странная ситуация (особенно на ADSL), что одни сайты работают, другие
ни в какую; или же короткие письма (комментарии в форму на сайт, etc.)
пролезают, длинные нет - это вот оно. На этот случай производители
кабельных модемов и всяких других решений уже давно делают "костыль" -
проходящим TCP-пакетам автоматически правится MSS на поменьше. И для
всех приложений, работающих по TCP, это прозрачно. Но здесь-то UDP...
Вот им и остается уповать на фрагментацию, которая резко увеличит
количество пакетов.
Далее, http://forum.bittorrent.org/viewtopic.php?id=131 сообщает,
что "Acks are required for every packet". В TCP давно уже научились
экономить на ответных пакетах, посылая их только когда надо, совсем не
на каждый. А сделано это затем, чтобы как можно точнее измерять
задержку между пакетами. Спрашивается, зачем минимизация задержки
приложению передачи файлов, ведь в TCP для bulk transfer через long fat
pipe давно уже есть алгоритмы?..
Тут и вылезает главная причина увеличения PPS и проблема протокола -
это сделано затем, чтобы уменьшить время между перепосылками при потере
пакетов - дескать, при шейпинге в буфере модема лучше пусть место
кончится для небольших пакетов, меньше перепосылать придется. Более
того, BEP-0029 прямо заявляет:
In order to have as little impact as possible on slow congested
links, uTP adjusts its packet size down to as small as 150 bytes per
packet. Using packets that small has the benefit of not clogging a
slow up-link, with long serialization delay. The cost of using
packets that small is that the overhead from the packet headers
become significant. At high rates, large packet sizes are used, at
slow rates, small packet sizes are used.
Другими словами, как только uTP обнаруживает потерю пакета вследствие
шейпинга или достижения ширины канала, он начинает уменьшать размер
пакета, что ведет к увеличению нагрузки, вследствие того еще большим
потерям и дальнейшему уменьшению размера пакета, вот такая больная
рекурсия. При этом и изначальный-то размер пакета невелик - всего 300
байт...
Но хватит о грустном. Решение для обычного не-туннельного уже
применяют, теперь пришла пора сделать для случая PPTP.
Составим выражение для tcpdump...
В предыдущем посте я описывал принципы работы BPF. Когда увидел в
теме на Наге просьбу аналога для PPTP, вспомнил, что у меня же была
похожая задача, там же и описана. В самом деле, там искалось DHT, здесь
просто другая сигнатура. Итак, назовём файл tcpdump-gre-utp-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 byte values to find in the UDP payload of inner IP datagram */
#define IS_TORRENT_UTP(udp_hdr_start) (ip[(udp_hdr_start+20):4]=0x7fffffff)
/* Check inner IP has UDP payload (proto 17) then calculate offset and pass it to UTP 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_UTP(ppp_hdr_len) (INNER_IS_UDP(ppp_hdr_len) and IS_TORRENT_UTP(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_IS_UTP(1)
) or (
(ip[GRE_DATA_START:2]=0xff03) and (ip[GRE_DATA_START+2]=0x21) and INNER_IS_UTP(3)
) or (
(ip[GRE_DATA_START:4]=0xff030021) and INNER_IS_UTP(4)
) or (
(ip[GRE_DATA_START:2]=0x0021) and INNER_IS_UTP(2)
)
)
Засунем это дело в скрипт из предыдущего поста, и...
Не работает. Не матчит. Запуск tcpdump -Xs 0 -ni em0 `cpp -P
tcpdump-gre-utp-cpp` тоже молчит. Значит, дело не в ng_bpf. Заменяю в
итоговом выражении UTP на UDP - начинает ловить все UDP-пакеты в
туннеле. Ага, значит дело в финальной части выражения. Делаю тестовый
пакет, выкусываю комментариями лишние ветки - всё равно не матчит.
Начинаю смотреть в tcpdump -d - там теперь всего 70 с небольшим
инструкций, это уже поддается анализу человеком. Ага, таки да -
обнаружен баг, tcpdump генерирует неверный код. Отправил баг-репорт
(см. kern/144325), но эта бага практически наверняка есть и на
линуксе - проект tcpdump отдельный...
Придется писать вручную
Что ж, придется применить знания по ассемблеру BPF и заняться тяжелым
ручным трудом. Правда, имея перед глазами файл для препроцессора,
перевести несколько проще, даже применяя оптимизации. Примерно 4 часа
работы, включая отладку, и вот что получилось:
bpf_prog_len=36 bpf_prog=[
{ code=177 jt=0 jf=0 k=0 } BPF_LDX+BPF_B+BPF_MSH X <- 4*(P[k:1]&0xf)
; X <- outer IP hdr length
{ code=64 jt=0 jf=0 k=0 } BPF_LD+BPF_W+BPF_IND A <- P[X+k:4]
{ code=84 jt=0 jf=0 k=0xff7fffff } BPF_ALU+BPF_AND+BPF_K A <- A & k
{ code=21 jt=0 jf=31 k=0x3001880b } BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt
: jf ; not VALID_PPTP_GRE, exit
{ code=80 jt=0 jf=0 k=1 } BPF_LD+BPF_B+BPF_IND A <- P[X+k:1]
; begin calc GRE hdr length
{ code=84 jt=0 jf=0 k=0x80 } BPF_ALU+BPF_AND+BPF_K A <- A & k
{ code=116 jt=0 jf=0 k=5 } BPF_ALU+BPF_RSH+BPF_K A <- A >> k
{ code=4 jt=0 jf=0 k=12 } BPF_ALU+BPF_ADD+BPF_K A <- A + k
{ code=12 jt=0 jf=0 k=0 } BPF_ALU+BPF_ADD+BPF_X A <- A + X
; now A = GRE_DATA_START
{ code=7 jt=0 jf=0 k=0 } BPF_MISC+BPF_TAX X <- A
{ code=72 jt=0 jf=0 k=0 } BPF_LD+BPF_H+BPF_IND A <- P[X+k:2]
{ code=21 jt=0 jf=3 k=0xff03 } BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt
: jf ; test PPP first part
{ code=135 jt=0 jf=3 k=0 } BPF_MISC+BPF_TXA A <- X
{ code=4 jt=0 jf=0 k=2 } BPF_ALU+BPF_ADD+BPF_K A <- A + k
{ code=7 jt=0 jf=0 k=0 } BPF_MISC+BPF_TAX X <- A
; now test 0x21 or 0x0021
{ code=80 jt=0 jf=0 k=0 } BPF_LD+BPF_B+BPF_IND A <- P[X+k:1]
{ code=21 jt=0 jf=3 k=0 } BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt
: jf ; test 0x00 (before 0x21)
{ code=135 jt=0 jf=3 k=0 } BPF_MISC+BPF_TXA A <- X
{ code=4 jt=0 jf=0 k=1 } BPF_ALU+BPF_ADD+BPF_K A <- A + k
{ code=7 jt=0 jf=0 k=0 } BPF_MISC+BPF_TAX X <- A
; that was 0x00, advance X
{ code=80 jt=0 jf=0 k=0 } BPF_LD+BPF_B+BPF_IND A <- P[X+k:1]
{ code=21 jt=0 jf=13 k=0x21 } BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt
: jf ; the final 0x21
{ code=135 jt=0 jf=3 k=0 } BPF_MISC+BPF_TXA A <- X
{ code=4 jt=0 jf=0 k=1 } BPF_ALU+BPF_ADD+BPF_K A <- A + k
{ code=7 jt=0 jf=0 k=0 } BPF_MISC+BPF_TAX X <- A
; now X points to inner IP
{ code=80 jt=0 jf=0 k=9 } BPF_LD+BPF_B+BPF_IND A <- P[X+k:1]
; protocol in inner IP
{ code=21 jt=0 jf=8 k=17 } BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt
: jf ; UDP ?
{ code=80 jt=0 jf=0 k=0 } BPF_LD+BPF_B+BPF_IND A <- P[X+k:1]
{ code=84 jt=0 jf=0 k=0x0f } BPF_ALU+BPF_AND+BPF_K A <- A & k
{ code=100 jt=0 jf=0 k=2 } BPF_ALU+BPF_LSH+BPF_K A <- A << k
; now A = inner IP hdr len
{ code=12 jt=0 jf=0 k=0 } BPF_ALU+BPF_ADD+BPF_X A <- A + X
{ code=7 jt=0 jf=0 k=0 } BPF_MISC+BPF_TAX X <- A
; now X points to inner UDP
{ code=64 jt=0 jf=0 k=20 } BPF_LD+BPF_W+BPF_IND A <- P[X+k:4]
; imagine inner_udp[20:4]=
{ code=21 jt=0 jf=1 k=0x7fffffff } 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
; match, -s 65535
{ code=6 jt=0 jf=0 k=0 } ] BPF_RET+BPF_K accept k bytes
; return 0 - non-match
Удалось уложиться всего в 36 инструкций, и без использования памяти
BPF-машины. Стало быть, будет работать быстро. Правда, реализовывать
проверку на то, что это пакет именно IPv4 и именно GRE, мне стало лень
- да и это может сделать файрвол. Скрипты можно взять в предыдущем
посте, хотя здесь они получатся меньше - tcpdump и awk не
задействованы. Протестировал нижеследующими командами, работает:
# kldload ng_ipfw
# ngctl mkpeer ipfw: bpf 127 ipfw
# ngctl name ipfw:127 utp_filter
# ngctl name ipfw:127 pptp_utp_filter
# ngctl msg pptp_utp_filter: setprogram '{ thisHook="ipfw"
ifMatch="matched" ifNotMatch="ipfw" bpf_prog_len=36 bpf_prog=[ {
code=0xb1 } { code=0x40 } { code=0x54 k=4286578687 } { code=0x15 jf=31
k=805406731 } { code=0x50 k=1 } { code=0x54 k=128 } { code=0x74 k=5 } {
code=0x4 k=12 } { code=0xc } { code=0x7 } { code=0x48 } { code=0x15
jf=3 k=65283 } { code=0x87 jf=3 } { code=0x4 k=2 } { code=0x7 } {
code=0x50 } { code=0x15 jf=3 } { code=0x87 jf=3 } { code=0x4 k=1 } {
code=0x7 } { code=0x50 } { code=0x15 jf=13 k=33 } { code=0x87 jf=3 } {
code=0x4 k=1 } { code=0x7 } { code=0x50 k=9 } { code=0x15 jf=8 k=17 } {
code=0x50 } { code=0x54 k=15 } { code=0x64 k=2 } { code=0xc } {
code=0x7 } { code=0x40 k=20 } { code=0x15 jf=1 k=2147483647 } {
code=0x6 k=65535 } { code=0x6 } ] }'
# sysctl net.inet.ip.fw.one_pass=0
# ipfw add 127 netgraph 127 gre from any to any via ng0
# ngctl msg pptp_utp_filter: getstats '"ipfw"'
Последняя строчка - снимает статистику. Вместо getstats можно еще
использовать getclrstats - тогда оно выведет статистику и атомарно
очистит её в ядре.
Ну и следует напомнить, что ввиду отличия спецификации от реальных
пакетов, это всё временная мера - можно ожидать, что в будущем
сигнатуры придется менять... и не спрашивайте меня, как - в предыдущем
посте было достаточно теории по этому вопросу :)