Решаем проблему «деградации» YouTube с помощью NoDPI
- воскресенье, 1 июня 2025 г. в 00:00:07
Салют, Хабр! Я думаю, каждый из вас знаком или, по крайней мере, слышал о такой прекрасной утилите как GoodbyeDPI (большое спасибо @ValdikSS!). Сегодня я хочу представить вам (почти) свою разработку - аналог GoodbyeDPI.
Около полугода назад в своей статье Обвиваем YouTube змеем, или как смотреть и скачивать видео с YouTube без VPN на чистом Python-е. Часть 1 (заблокирована по требованию РКН) я рассказывал о том, как скачивать видео с YouTube на Python, а так как YouTube у нас "деградировал", я поделился инструментом, с помощью которого можно исправить этот вопиющий недостаток. Инструмент тогда выглядел достаточно сыро, и несмотря на то, что он выполнял свою задачу, требовал серьезных улучшений. Увидев интерес общественности, я решил допилить его, и вот, спустя полгода, после немалой работы, я рад представить вам NoDPI - проект, который явно не понравится РКН (и он опять меня заблокирует).
В этой статье я хочу рассказать о его возможностях, внутреннем устройстве, отличии от аналогов. Надеюсь, статья будет вам полезна и интересна. Поехали!
Существует множество проектов, которые позволяют бороться с DPI и обходить блокировки.
NoDPI - это программа, которая играет роль локального прокси-сервера. Она обрабатывает проходящий через нее трафик таким образом (каким - чуть ниже), что это позволяет сбивать с толку системы DPI, установленные у провайдера. Она отлично справляется с "разблокировкой" всех сайтов, к которым ограничен доступ с помощью DPI. Исключение составляют сайты на HTTP (незащищенном протоколе) и сайты, которые не поддерживают TLS v1.3.
Около года назад я искал (без особой надежды) какой-нибудь простенький инструмент, альтернативу GoodbyeDPI и ему подобных, для встраивания в свой пет-проект. К удивлению, такой инструмент нашелся - https://github.com/theo0x0/nodpi. Это было как раз то, что мне нужно - небольшой скриптик на python. Правда работал он не очень стабильно, в коде был полный бардак, и все это требовало доработки. После тестирования в проекте он мне понравился и я стал использовать его в повседневной жизни для просмотра YouTube и иже с ним. К удивлению, на моем провайдере он оказался лучше, чем GoodbyeDPI. Потом я рассказал о нем в статье и, увидев интерес, стал развивать его - улучшил алгоритм обхода DPI, разобрался с ошибками, прикрутил красивый TUI и кучу мелких фишек. Вобщем, превратил его в более-менее нормальную консольную утилиту с приятным интерфейсом.
Принцип работы довольно простой и понятный, а минимальная работающая реализация занимает всего 110 строк кода (сейчас, со всеми улучшениями, она разрослась до 480). Весь проект полностью написан на Python без использования сторонних библиотек.
Итак, NoDPI представляет собой асинхронный прокси-сервер, реализованный на базе библиотеки asyncio
. Мы создаем сервер с помощью asyncio.start_server()
и направляем через него HTTPS-трафик (HTTP обрабатываться не будет и просто пропускается дальше) и программа действует по следующему алгоритму:
Извлекает из заголовка запроса целевой хост и порт, а затем открывает соединение и отправляет клиенту статус "OK".
client_ip, client_port = writer.get_extra_info("peername")
http_data = await reader.read(1500)
if not http_data:
writer.close()
return
headers = http_data.split(b"\r\n")
first_line = headers[0].split(b" ")
method = first_line[0]
url = first_line[1]
if method == b"CONNECT":
host_port = url.split(b":")
host = host_port[0]
port = int(host_port[1]) if len(host_port) > 1 else 443
else: # Для HTTP
host_header = next(
(h for h in headers if h.startswith(b"Host: ")), None
)
if not host_header:
raise ValueError("Missing Host header")
host_port = host_header[6:].split(b":")
host = host_port[0]
port = int(host_port[1]) if len(host_port) > 1 else 80
if method == b"CONNECT":
writer.write(b"HTTP/1.1 200 OK\r\n\r\n")
await writer.drain()
remote_reader, remote_writer = await asyncio.open_connection(
host.decode(), port
)
Если тип соединения "CONNECT" (т. е. это HTTPS) мы перехватываем tls-рукопожатие (handshake) и отправляем его на фрагментацию.
Напомню, как выглядит tls запись для протокола handshake:
Здесь первый (нулевой) байт несет информацию о типе содержимого (для handshake это 22 в dec или 0x16 в hex). Затем идет байт для major version (всегда 0x03) и minor version, которая обозначает конкретную версию и может варьироваться от 0 до 4:
После этого идет вся полезная нагрузка - в данном случае нас она не интересует.
Что делает программа? Она тупо разбивает пэйлоад на несколько кусков случайного количества и случайной длины, и склеивает с байтовой последовательностью \x16\x03\x04
(+ data). Т. е. одна tls запись превращается в несколько записей разной длины. После этого они объединяются и отправляются как один пакет:
try:
head = await reader.read(5)
data = await reader.read(2048)
except Exception as e:
...
if all(site not in data for site in self.blocked):
writer.write(head + data)
await writer.drain()
return
parts = []
host_end = data.find(b"\x00")
if host_end != -1:
parts.append(
bytes.fromhex("160304")
+ (host_end + 1).to_bytes(2, "big")
+ data[: host_end + 1]
)
data = data[host_end + 1:]
while data:
chunk_len = random.randint(1, len(data))
parts.append(
bytes.fromhex("160304")
+ chunk_len.to_bytes(2, "big")
+ data[:chunk_len]
)
data = data[chunk_len:]
writer.write(b"".join(parts))
await writer.drain()
Замечу, что эта фрагментация применима только к https-трафику, так как в http, понятное дело, tls-а нет.
И все! Все остальные данные отправляются "как есть", без какой-либо обработки:
while not reader.at_eof() and not writer.is_closing():
data = await reader.read(1500)
writer.write(data)
await writer.drain()
Честно говоря, я до сих пор не очень понял, почему сайтам пофиг на такой формат, но это работает! И оно работает даже если в поле minor version вставлять абсолютно случайный байт.
Из основных фич утилиты я бы выделил следующие пункты:
Приятный и понятный консольный интерфейс с подсчетом трафика и скорости:
Полное логирование ошибок и доступа (какой ip куда обращался)
Программа не требует админских прав (как GoodbyeDPI).
Программа не требует настройки. Все, что нужно - указать список доменов и включить прокси в браузере или системе.
Так как это прокси, то не нужно пускать через него весь трафик, достаточно указать выбранные домены, если вам это нужно.
Инструмент не лезет в потроха системы, не устанавливает хуков, не требует драйверов и пр.
Кроссплатформенность. Гарантированно работает на Windows и Linux (Android пока не тестировался).
Инструмент тестировался на провайдерах МТС и ТТК на компьютерах с ОС Windows и Linux. В 98% случаях никаких проблем не возникает. Иногда, при резких перепадах скорости в сети, наблюдается зависание видео; проблема решается перезагрузкой страницы. Также, есть проблемы с работой HTTP-сайтов в Firefox - в некоторых случаях отображается предыдущая страница, а не запрашиваемая в данный момент. Исследование проблемы находится в процессе.
Весь исходный код и бинарники вы можете найти на моем GitHub-е: https://github.com/GVCoder09/nodpi/ Там же находится подробная инструкция по запуску. Я искренне надеюсь, что эта программа принесет вам пользу, или, по крайней мере, заинтересует ее идея. Если вы хотите поддержать меня, то это можно сделать единственным способом - поставить плюсик статье :-)
Ну и конечно, я буду рад, если кто-то присоединится к разработке - issues и пул реквесты приветствуются)
С разрешения модераторов статья публикуется в профильных хабах, без хаба "Я пиарюсь"