Vadim Goncharov (nuclight) wrote,
Vadim Goncharov
nuclight

Categories:

Фильтрация 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 - тогда оно выведет статистику и атомарно очистит её в ядре.

Ну и следует напомнить, что ввиду отличия спецификации от реальных пакетов, это всё временная мера - можно ожидать, что в будущем сигнатуры придется менять... и не спрашивайте меня, как - в предыдущем посте было достаточно теории по этому вопросу :)
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 

  • 21 comments