NoDPI4Android. Решаем проблему «деградации» YouTube теперь и на Android

от автора

Салют, Хабр! На связи снова я, Aragorn, со своим проектом по терроризированию Роскомпозора. В прошлый раз я рассказывал о NoDPI — утилите для «раздеградирования» YouTube и установил личный рекорд — 400 звезд на GitHub и блокировка статьи РКН через три дня после публикации.

Многие мои знакомые и люди в комментариях просили сделать версию под Android и Android TV. Я не очень дружу с Джавой и с Джавой под андроид в особенности, и поэтому такая перспектива меня не очень прельщала, но у меня был опыт написания android-приложений на python и kivy, который я и решил применить. После нескольких дней (и ночей) напряженного труда и танцев с бубном, мне наконец удалось создать NoDPI for Android, который практически не имеет аналогов. Именно о нем я и хочу сегодня рассказать. Надеюсь, статья будет вам полезна и интересна. Поехали!


Я, конечно, не дизайнер, но получилось вроде не плохо

Я, конечно, не дизайнер, но получилось вроде не плохо

Немного про потроха

NoDPI4Android — это графическая надстройка над NoDPI. Как работает сам NoDPI я подробно рассказывал в прошлой статье, но так как без V*N её теперь не почитаешь, то вкратце повторю.

NoDPI представляет собой асинхронный прокси-сервер на базе библиотеки asyncio Он перехватывает tls-рукопожатия (handshake) исходящих соединений и отправляет их на фрагментацию. Если домен присутствует в списке заблоченных, программа разбивает пэйлоад на несколько кусков случайного количества и случайной длины, и склеивает с байтовой последовательностью \x16\x03\x04 (+ data). Т. е. одна tls запись превращается в несколько записей разной длины. После этого они объединяются и отправляются как один пакет. Пока у DPI нет мощностей, чтобы разбираться с таким хаосом в пакетах, и все это благополучно следует к пункту назначения, а мы, довольные, смотрим YouTube.

Как я уже упомянул, NoDPI4Android написан исключительно на python. Кто-то покрутит у виска и скажет, что писать под android на python — это безумие. Да, возможно это не самый подходящий язык для таких целей, но у него есть и ряд преимуществ. В первую очередь, это простота написания и сборки простых приложений, которые не взаимодействуют с сервером и не требуют фоновой активности. Все такие приложения пишутся с использованием фреймворка Kivy, который предоставляет широкие возможности для создания UI и даже имеет свой декларативный язык разметки KV-lang. Чуть выше Kivy стоит KivyMD, который предоставляет множество различных виджетов в стиле Material Design. А работу всего этого на Android обеспечивает python-for-android (p4a), который собирает нативный CPython + NDK + код в APK.

Наше приложение разделено на две части — непосредственно приложение (main.py), с которым взаимодействует пользователь, и сервис (service.py), в котором работает прокси.

В приложении используется Kivy и KivyMD и ничего сложного в нем нет — одна графика: кнопка запуска/остановки сервиса, редактирование настроек прокси и черного списка. Для запуска сервиса приходиться немного воспользоваться API Android:

from android import mActivity from jnius import autoclass  def start_service(name_service: str) -> None:      context = mActivity.getApplicationContext()     service = autoclass(str(context.getPackageName()) + ".Service" + name_service)     service.start(mActivity, "")

При этом за само создание сервиса отвечает p4a и для этого в конфиг сборки надо добавить всего лишь одну строчку с указанием входной точки и параметрами:

services = Proxy:%(source.dir)s/service.py:foreground:sticky

С сервисом немного сложнее. Чтобы Android не прибивал его, мы используем foreground service (а не background), который требует постоянного наличия уведомления. Само уведомление отправлять не надо — система сделает это самостоятельно. Логику прокси я портировал из NoDPI без изменений, изменился лишь способ его запуска:

...  class ProxyServer:     def __init__(self):         if os.path.exists(os.path.join(app_storage_path(), "proxy_config.json")):             try:                 with open(os.path.join(app_storage_path(), "proxy_config.json"), "r", encoding="utf-8") as f:                     config = json.load(f)                     self.host = config.get("host", "0.0.0.0")                     self.port = str(config.get("port", "8881"))             except Exception as e:                 self.host = "0.0.0.0"                 self.port = "8881"         else:             self.host = "0.0.0.0"             self.port = "8881"         self.server = None         self.loop = None         self.running = False      def start(self):          if self.running:             return          self.running = True         self.thread = threading.Thread(target=self._run_server, daemon=True)         self.thread.start()      def _run_server(self):          try:             self.loop = asyncio.new_event_loop()             asyncio.set_event_loop(self.loop)              async def server_main():                 self.server = await asyncio.start_server(                     new_conn, self.host, self.port                 )                  async with self.server:                     await self.server.serve_forever()              self.loop.run_until_complete(server_main())         except Exception as e:             pass         finally:             if self.loop and self.loop.is_running():                 self.loop.stop()             if self.loop:                 self.loop.close()             self.running = False      def stop(self):          if not self.running:             return          self.running = False          if self.server:             self.server.close()             if self.loop and self.loop.is_running():                 self.loop.call_soon_threadsafe(self.loop.stop)  if __name__ == '__main__':    proxy = ProxyServer()   proxy.start()    while proxy.running:     threading.Event().wait(1) 

Без запуска прокси в Thread сервис почему-то дохнет, а чтобы работа основного потока не завершалась приходится делать threading.Event().wait(1)

Все! Остается только настроить конфигурацию сборки, которая осуществляется с использованием инструмента buildozer:

buildozer.spec
[app]  source.dir = ./src source.include_exts = py,png,jpg,kv,txt version = 1.0 requirements = kivy,https://github.com/kivymd/kivymd/archive/master.zip,android,pyjnius,materialyoucolor,pillow,asynckivy,asyncgui  presplash.filename = ./assets/presplash.png icon.filename = ./assets/ico.png orientation = portrait fullscreen = 0  [android]  title = NoDPI package.name = nodpi package.domain = com.gvcoder  services = Proxy:%(source.dir)s/service.py:foreground:sticky android.permissions = INTERNET,FOREGROUND_SERVICE,POST_NOTIFICATIONS android.accept_sdk_license = True  [buildozer]  log_level = 1

Запускаем…

buildozer android debug

…и на выходе получаем готовый APK.

Настройка и использование

NoDPI4Android работает на Android 5.0 и выше. После установки приложения (скачать его можно здесь), нужно дать некоторые разрешения и настроить прокси на вашем устройстве.

  1. Откройте приложение и нажмите на кнопку START SERVER. Затем дайте разрешение на отправку уведомлений. Без этого приложение работать не будет!

    Скрытый текст
  2. В настройках приложения отключите оптимизацию, в противном случае Android прибьет сервис через некоторое время (особенно это характерно для MIUI)

    Скрытый текст
  3. Ну и самое главное — настройка прокси. В большинстве оболочек прокси можно настроить только для WiFi, но в MIUI такая функция доступна и для мобильного интернета. В OneUI прокси включается так:

    Скрытый текст

    В имени узла прокси у меня стоит 127.0.0.1, но по умолчанию приложение использует 0.0.0.0, поэтому вводить нужно именно его

  4. Все! Теперь можно наслаждаться просмотром. Если с первого раза не заводится, перезапустите приложение и сервер кнопкой START SERVER/STOP SERVER

Известные проблемы

  1. Сервис падает в оболочке MIUI Несмотря на отключение оптимизации, в MIUI сервис почему-то дольше 12 часов не живет и требуется его постоянный перезапуск. Я не знаю с чем это связано, так как в OneUI он проработал две недели без сбоев.

  2. Большой размер приложения Сам APK занимает около 40МБ, а после установки и использования размер вырастает до 100МБ. Это ключевой недостаток разработки на Kivy и связан он с тем, что приложение тащит за собой CPython и все зависимости, которые у него есть.

  3. Невозможно изменить адрес прокси (кнопка SETUP PROXY) Эта проблема наблюдается с клавиатурой MIUI и я никак не могу повлиять на нее. Единственный способ решения — выделить текст и начать вводить символы.

Аналоги

Заключение

Весь исходный код и APK вы можете найти на моем GitHub-е: https://github.com/GVCoder09/NoDPI4Android Там же находится подробная инструкция по сборке приложения в apk. Я искренне надеюсь, что эта программа принесет вам пользу, или, по крайней мере, заинтересует ее идея. Если вы хотите поддержать меня, то это можно сделать единственным способом — поставить плюсик статье 🙂

Ну и конечно, я буду рад, если кто-то присоединится к разработке — issues и пул реквесты приветствуются)


ссылка на оригинал статьи https://habr.com/ru/articles/923566/