Перевод этой страницы:
ru

Инструменты пользователя

Инструменты сайта


ru:software:article:utp_dpi

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

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

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

Жизнь с uTP

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

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

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

Ещё одним неприятным моментом в экспериментах с uTP на начальных этапах было то, что он генерировал большую пакетную нагрузку, отправляя множество мелких UDP пакетов. Позднее авторы научились заполнять пакеты с данными целиком.
Повышение пакетрейта губительно сказывается на WiFi и прочих радиолинках.

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

Протокол 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 пакета заменяются данные, пересчитывается контрольная сумма и от отправляется дальше.

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

Сигнатуры

Администраторы искали способ как быстро нормализовать работу сети и решили фильтровать uTP по сигнатурам пакетов, добавляя их то в ACL коммутаторов то в фаервол BSD/Linux роутера.
«Странность» ситуации в том, что сигнатуры искали анализируя пакеты.
Притом, что код libuTP был открыт 16 мая 2010 года - через 4 месяца после выхода uTorrent 2.0 где uTP был включён.

Спустя пару месяцев «живительные» сигнатуры путём нечеловеческих усилий по анализу пакетов были получены.
Ещё через некоторое время авторы uTP поменяли пару незначительных для протокола начальных значений в SYN пакете и что то рандомизировали: connid и seq_nr - больше не смогли.

После того как ng_utp был написан стало понятно что проверять корректность работы с помощью tcpdump без правильных сигнатур мягко говоря не удобно - слишком много лишнего приходилось пробегать глазами.
Я ещё раз пробежался по коду libuTP и получились такие сигнатуры, сейчас может быть они уже устарели.

Версия 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. uTP имеет достаточно чёткие сигнатуры и легко ловится DPI.
Более того, ловить сигнатуры в TCP ощутимо сложнее, поскольку для гарантированного обнаружения нужно уметь собирать несколько пакетов вместе и уже потом проверять содержимое: клиент может передавать данные по одному байту.
Авторы uTP либо не ставили себе цель сделать протокол без сигнатур либо даже не приблизись к цели.
На мой взгляд в начале не ставили, а потом было уже поздно и рандомизация отдельных полей не помогает.

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 торренты пустят и через него, скорее всего это вопрос времени.

ru/software/article/utp_dpi.txt · Последние изменения: 2015/05/27 23:28 — root