Site Tools


software:article:utp_dpi

This is an old revision of the document!


Torrent/uTP — о протоколе и самодельных DPI

В 2009 году появился Micro Transport Protocol, сокращённо - uTP, можно ознакомится тут.
Суть задумки в том, чтобы не полагаться на TCP Congestion Control хубр, которым под windows рулить весьма проблематично, а самим управлять загрузкой канала.
Другая причина это возможность приёма входящего соединения через NAT.
uTP выявил много узких мест как у провайдеров так и у пользователей: ещё вчера прекрасно работающие роутеры превратились в тыкву. А некоторые пользователи обнаружили что торренты качаются на все 100 мегабит, не зависимо от тарифа.

Вот какой бред написан в википедии:

Также провайдерам намного сложнее блокировать передачу данных через μTP благодаря отсутствию строгих, формализованных отличий UDP пакетов обычного трафика (формируемого, к примеру, сетевыми играми) от трафика, формируемого протоколом μTP, в отличие от TCP пакетов, по содержанию полей которых можно делать вывод об их принадлежности к p2p-трафику.

Как не правильно блокировать можно почитать тут: http://geektimes.ru/post/243305/ и немного ниже :)

Жизнь с uTP

<spoiler title=“Слухи, сплетни, домыслы”> В адрес авторов uTP звучала масса упрёков в изобретении TCP с нуля и хождении по всем граблям, в том что они не взяли уже готовые протоколы, и в том что теперь придётся обновляться и расширятся. С точки зрения разработчиков - выбора особо не было: TCP все провайдеры шейпят и душат, для управления всеми аспектами работы tcp протокола в винде нужны права администратора и скорее всего свой драйвер, многие другие протоколы которые ходят поверх IP (tcp/udp/gre/udplite/…) вообще провайдерами фильтруются и в винде их так просто не реализовать. Потому просто взяли и сделали поверх UDP.

Это решение подкосило многие домашние мыльницы и некоторых провайдеров. Количество трансляций в NAT роутеров стало очень быстро расти. Для TCP - NAT знает когда соединение установлено и когда оно завершено, а для UDP понятие соединений отсутствует в принципе, поэтому обычно применяются таймеры для удаления старых сессий.

Другим побочным эффектом явилось то, что uTorrent запрашивал больше трафика чем позволял тарифный план провайдера, и от этого страдали даже те провайдеры у которых шейпер был настроен правильно: на хомячка из интернета прилетало ощутимо больше его тарифного плана и этот излишек дропался шейпером. Провайдеры несли финансовые потери от такого DDoS хомяка на самого себя. Авторы uTorrent позже всё таки научились правильно подстраиваться под канал, но их эксперименты стоили нервов и денег.

Ещё одним неприятным моментом в экспериментах с uTP на начальных этапах было то, что он генерировал большую пакетную нагрузку, <a href=“http://torrentblog.ru/protokol-utp-ne-sderzhal-svoix-obeshhanij/”>отправляя множество мелких UDP пакетов</a>. Позднее авторы научились заполнять пакеты с данными целиком. Повышение пакетрейта губительно сказывается на WiFi и прочих радиолинках.

В целом, протокол оказал ощутимое влияние как на провайдеров так и на производителей железа, я бы даже сказал что он подготовил почву для HD в ютубе.</spoiler>

Протокол uTP

Версия 0

Начиная с uTorrent 1.8

typedef struct utp_pkt_s { 		/* offset - PacketFormat */
	uint32_t	connid;		/* 00 connection ID */
	uint32_t	tv_sec;		/* 04  */
	uint32_t	tv_usec;	/* 08  */
	uint32_t	reply_micro;	/* 12  */
	uint8_t		windowsize;	/* 16 receive window size in PACKET_SIZE chunks */
	uint8_t		ext;		/* 17 Type of the first extension header */
	uint8_t		flags;		/* 18 Flags */
	uint16_t	seq_nr;		/* 19 Sequence number */
	uint16_t	ack_nr;		/* 21 Acknowledgment number */
					/* 23 ext/data */
} utp_pkt_t, *utp_pkt_p; /* 23 bytes */

Версия 1

Начиная с uTorrent 2.0

typedef struct utp_pkt_v1_s { 		/* offset - PacketFormatV1 */
	uint8_t		version:4;	/* 00 protocol version */
	uint8_t		type:4;		/* 00 type (formerly flags) */
	uint8_t		ext;		/* 01 Type of the first extension header */
	uint16_t	connid;		/* 02 connection ID */
	uint32_t	tv_usec;	/* 04  */
	uint32_t	reply_micro;	/* 08  */
	uint32_t	windowsize;	/* 12 receive window size in bytes */
	uint16_t	seq_nr;		/* 16 Sequence number */
	uint16_t	ack_nr;		/* 18 Acknowledgment number */
					/* 20 ext/data */
} utp_pkt_v1_t, *utp_pkt_v1_p; /* 20 bytes */

Типы пакетов

enum {
	ST_DATA		= 0,	/* Data packet. */
	ST_FIN		= 1,	/* Finalize the connection. This is the last packet. */
	ST_STATE	= 2,	/* State packet. Used to transmit an ACK with no data. */
	ST_RESET	= 3,	/* Terminate connection forcefully. */
	ST_SYN		= 4,	/* Connect SYN. */
	ST_NUM_STATES		/* Used for bounds checking. */
};

flags из версии 0 превратился в type в версии 1, типы пакетов перечислены выше.
Сначала отправляется SYN на него приходит ответ STATE или RESET.
Завершается соединение на FIN или RESET.
DATA и STATE используются при передаче данных.

connid - идентификатор соединения. В TCP его роль выполняет номер порта (вернее их пара). Номер соединения у двух хостов всегда различается на единицу.

Вообще довольно запутанная и странная схема установления соединения:

SYN: connid=34 - запрос на установление соединения

« STATE: connid=34 - подтверждение

DATA: connid=35 - передача данных

« STATE: connid=34 - подтверждение передачи данных
Те инициатор соединения задаёт номер соединения в первом пакете а в дальнейшем использует номер на единицу больше.

seq_nr и ack_nr - используется чтобы ориентироваться в потоке в случае потери или реордеринга (когда первый отправленный пакет приходит после второго).

Остальные поля меня интересовали мало, хотя для ext опций валидацию написал.

ext - если есть дополнительные расширения/данные в пакете после заголовка, аналог IP otions.

tv_usec, reply_micro, windowsize - относятся к информации необходимой для управления скоростью передачи.

Шифрование

Шифрования нет. Совсем.
Не потому что описание не полное а потому что оно реализовано несколькими уровнями выше и uTP никак не касается.
uTP заголовки никак не шифруются.

Фильтрация / распознавание uTP

Заход 1: uTPControl

Из спортивного интереса я решил попробовать написать нечто что сможет аккуратно выключать uTP у юзеров чей трафик проходит через роутер с моей программой.
В начале я пробовал слать RESET с виндовой машины, перебирая connid, но это явно не работало, не совпадали адреса отправителя пакета и seq_nr, ack_nr. Это был первый неудачный опыт.

uTPControl - block uTP torrent proto - была первая более менее программа которую я написал под FreeBSD.
uTP протокол был любезно предоставлен в libuTP всему интернету :)

Работало uTPControl чрезвычайно просто: программа создавала divert сокет и бесконечно читала из него пакеты в цикле. Если это был UDP пакет, в котором ВОЗМОЖНО uTP версии 0 или 1 и тип пакета не FIN и не RESET то генерируем UDP-uTP RESET пакет и отправляем обратно.
Те клиент пытался установить связь и сразу получал RESET - те его вроде как отключил тот к кому он подключался.

Минусов у этого решения было два:
1. Ложные срабатывания: иногда пакеты от некоторых онлайн игр были прямо как uTP и им улетал ответ, видимо у игр от этого срывало крышу и юзеры жаловались.

2. Низкая производительность: все пакеты из ядра копировались в юзерспейс и там в один поток обрабатывались. В один поток на CoreDuo Е5300 под FreeBSD 7.3 выдавало до 100 тысяч пакетов RESET в секунду.
Один поток можно было обойти запустив несколько экземпляров и как то раскидав через ipfw пакеты между ними.

В виду этих фатальных недостатков интерес со стороны сообщества пропал и я её забросил.

Был человек который превратил это в netgraph ноду, но ложные срабатывания это не вылечило.

Заход 2: ng_utp

FreeBSD uTP (udp torrent) netgraph node
Прошло полтора года, я успел покопаться в ядре FreeBSD и netgraph, лучше узнать как работает сеть и пришла мысль: uTP имеет состояния аналогичные TCP, значит чтобы его 100% определять нужно эти состояния отслеживать.
Заодно я ещё раз заглянул в libuTP и получше посмотрел за что можно зацепится.
За сигнатуры я решил не цепляться, это плохой путь с массой ложных срабатываний и мучениями по их поддержанию - авторы уже несколько раз меняли начальные константы и сигнатуры “протухали” у тех кто их использовал.
Идеальный вариант это свой “клиент” с референсной реализацией uTP который будет выстраивать таблицу соединений на основе пролетающих через него пакетов и уже по данным этой таблицы что то можно делать.

В итоге получилась netgraph нода, которую можно подключать к L2 хукам типа ng_ether или L3 хукам, например ng_ipfw. В первом случае можно вообще сделать прозрачный эзернет мост из двух сетевух (не обязательно физических). Ещё можно просто поставить тазик и зеркалировать на него весь траф, но я сейчас не уверен в работоспособности такой схемы.

Результатов замеров производительности я не сохранил.
Однако нода без проблем параллелится по ядрам, может выполнятся как контексте ISR так и потоками netgraph, взаимные блокировки потоков сведены к минимуму.

В случае L2: мультикаст и броадкаст пакеты пролетают сразу насквозь, тэгированный трафик обрабатывается как обычный. QinQ не делал, добавить не сложно.
Обрабатываются только IPv4 пакеты UDP, не адресованные и не отправленные с 127/8, не являющиеся броадкастом или мультикастом, и ещё немного проверок что это не мусор а то что нужно, включая опциональную проверку контрольной суммы IP и UDP.

Далее проверяется что содержимое UDP пакета похоже на uTP.
Если содержимое похоже то ищем в таблице состояний запись для данной пары хостов: src ip:port / dst ip:port, если не находим то меняем местами и ищем ещё раз. Не нашли и пакет не FIN или RESET - добавляем. (подозреваю что на линухе с conntrack было бы чуть проще, а тут пришлось самому писать)
Теперь есть элемент который хранит все uTP соединения между двумя хостами или то что похоже на них. Ищем там connid, если не нашли то connid+-1, если опять нет - добавляем.

Теперь у нас есть куда писать данные по конкретному uTP соединению.
Пишем: время последнего обновления, время последней отправки запроса и получения ответа, считаем запросы и ответы, запоминаем какие типы пакетов встречались.
seq_nr и ack_nr можно было бы тоже запоминать и анализировать но и без них достаточно точно получается определять.
Старые записи удаляются автоматически.

Теперь мы точно знаем что хосты установили uTP соединения, знаем сколько каждый отправил пакетов и получил ответов, знаем их идентификаторы, можно действовать.

Действовать имеет смысл только для DATA и STATE пакетов.

Что можно сделать: <ul><li>ничего, просто мониторинг: есть счётчики по хостам, соединениями, сами таблицы с хостами…</li> <li>дропать пакеты с uTP: src ip:port + dst ip:port есть</li> <li>помечать пакеты: на L2 в VLAN заголовке PCP для тегированных пакетов 802.1P</li> <li>помечать пакеты: на L3 в IP заголовке - ip_tos - DSCP</li> <li>отправлять uTP - RST пакеты</li></ul> Чтобы сгенерировать RST пакет все данные есть: src ip:port + dst ip:port, pkt_ver, connid, ack_nr, seq_nr. Фактически у IP/UDP пакета заменяются данные, пересчитывается контрольная сумма и от отправляется дальше.

Подробнее про счётчики - по <a href=“http://www.netlab.linkpc.net/forum/index.php?topic=804.0”>ссылке</a>, там описание, если его мало есть код :)

Сейчас это всё ещё может быть актуальным для различных беспроводных сетей и офисных сетей, остальные уже обновились и расширились.

Сигнатуры

Провайдеры искали способ как быстро нормализовать работу сети и решили фильтровать uTP по сигнатурам пакетов, добавляя их то в ACL коммутаторов то в фаервол BSD/Linux роутера.
“Странность” ситуации в том, что сигнатуры искали анализируя пакеты.
Притом, что код <a href=“https://github.com/bittorrent/libutp”>libuTP</a> был открыт 16 мая 2010 года - через 4 месяца после выхода uTorrent 2.0 где uTP был включён.

Спустя пару месяцев “живительные” сигнатуры путём нечеловеческих усилий по анализу пакетов были <a href=“http://www.opennet.ru/tips/info/2304.shtml”>получены</a>.
Ещё через некоторое время авторы поменяли пару незначительных для протокола начальных значений в SYN пакете и что то <a href=“http://www.linux.org.ru/forum/admin/6357142”>рандомизировали</a> (connid, seq_nr - больше не смогли) :)</spoiler>

После того как ng_utp был написан стало понятно что проверять корректность работы с помощью tcpdump без правильных сигнатур мягко говоря не удобно - слишком много лишнего приходилось пробегать глазами. Я ещё раз пробежался по коду <a href=“https://github.com/bittorrent/libutp”>libuTP</a> и получились такие сигнатуры, сейчас может быть они уже устарели.

Версия 0

SYN

syn - 14 bytes:

udp[17] = 2 and udp[18] = 4 and udp[21:2] = 0 and udp[23] = 0 and udp[24] = 8 and udp[25:4] = 0 and udp[29:4] = 0

41 = udp hdr len (8) + upd pkt data len

upd header included:

(udp[4:2] = 41 and udp[25:2] = 0x0204 and udp[29:4] = 0x00000008 and udp[33:4] = 0 and udp[37:4] = 0)

- последнее это то что можно скармливать в tcpdump, отличается от первой смещениями и тем что константы объединены чтобы сравнений было меньше. Первая больше для самообразования.

RESET

rst - 4 bytes:

udp[17] = 0 and udp[18] = 3

upd header included:

(udp[4:2] = 31 and udp[25:2] = 0x0003)

Версия 1

SYN

syn - 14 bytes:

udp[0] & 0x0f = 1 and udp[0] & 0xf0 = 0x40 and udp[1] = 2 and udp[18:2] = 0 and udp[20] = 0 and udp[21] = 8 and udp[22:4] = 0 and udp[26:4] = 0

“(udp[0] & 0x0f = 1 and udp[0] & 0xf0 = 0x40)” можно преобразовать в: “udp[0] = 0x41”
upd header included:

(udp[4:2] = 38 and udp[8:2] = 0x4102 and udp[26:4] = 0x00000008 and udp[30:4] = 0 and udp[34:4] = 0)

RESET

rst - 4 bytes:

udp[0] & 0x0f = 1 and udp[0] & 0xf0 = 0x30 and udp[1] = 0

“udp[0] & 0x0f = 1 and udp[0] & 0xf0 = 0x30” можно преобразовать в: “udp[0] = 0x31”
upd header included:

(udp[4:2] = 28 and udp[8:2] = 0x3100)

Обнаружение фильтрации

Проще всего, используя описание протокола, реализовать простенький клиент, который будет устанавливать соединение и пытаться отправлять данные.
По сути нужно симулировать установление соединения, и дальше пытаться слать DATA и STATE пакеты в ответ с ext типа ACK.
Дальше один клиент запускается в интернете, другой у себя и смотрим теряются ли пакеты в 100% случаев или может RESET приходят.
Сходным образом при использовании yota некоторые пакеты из l2tp на завершающем этапе согласования пропадают в 100% случаев. Так было ещё в сентябре 2014.

Заключение

1. То что <a href=“https://ru.wikipedia.org/wiki/ΜTorrent”>написано в вики на русском</a> - полнейший бред: uTP имеет достаточно чёткие сигнатуры и легко ловится DPI. Более того, ловить сигнатуры в TCP ощутимо сложнее, поскольку для гарантированного обнаружения нужно уметь собирать несколько пакетов вместе и уже потом проверять содержимое: клиент может передавать данные по одному байту. Авторы uTP либо не ставили себе цель сделать протокол без сигнатур либо даже не приблизись к цели. (На мой взгляд в начале не ставили, а потом было уже поздно и рандомизация отдельных полей не помогает). <a href=“http://en.wikipedia.org/wiki/Micro_Transport_Protocol”>Вики на английском</a> более адекватна.

2. Производители различных DPI уже давно добавили сигнатуры для uTP, вряд ли им это было трудно сделать.

3. В порядке слухов: для линукса вроде бы тоже есть ядерная версия для работы с uTP протоколом на базе ipp2p а может уже отдельно. Но в паблик её не выкладывали. С середины 2012 года.

4. Для IPv6 код не писал, на всякий случай ;)

5. uTP не лучше TCP для передачи данных, вся проблема в том, что TCP можно хоть как то управлять из приложения только на BSD/Linux - setsockopt(…, IPPROTO_TCP, TCP_CONGESTION,…) - основное что требуется, хотя и там более тонкие параметры congestion control для отдельных сокетов не настраиваются. Говорить про оверхэд в 23/20 байт сейчас уже не актуально, HTTP/2.0 не сильно лучше. Возможно с приходом кучи готовых либ для HTTP/2.0 торренты пустят и через него, скорее всего это вопрос времени.

software/article/utp_dpi.1432318232.txt.gz · Last modified: 2015/05/22 18:10 by root