
Салют, Хабр! На связи снова я, 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 и выше. После установки приложения (скачать его можно здесь), нужно дать некоторые разрешения и настроить прокси на вашем устройстве.
Откройте приложение и нажмите на кнопку START SERVER. Затем дайте разрешение на отправку уведомлений. Без этого приложение работать не будет!
Скрытый текст

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

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

В имени узла прокси у меня стоит 127.0.0.1, но по умолчанию приложение использует 0.0.0.0, поэтому вводить нужно именно его
Все! Теперь можно наслаждаться просмотром. Если с первого раза не заводится, перезапустите приложение и сервер кнопкой START SERVER/STOP SERVER
Известные проблемы
Сервис падает в оболочке MIUI Несмотря на отключение оптимизации, в MIUI сервис почему-то дольше 12 часов не живет и требуется его постоянный перезапуск. Я не знаю с чем это связано, так как в OneUI он проработал две недели без сбоев.
Большой размер приложения Сам APK занимает около 40МБ, а после установки и использования размер вырастает до 100МБ. Это ключевой недостаток разработки на Kivy и связан он с тем, что приложение тащит за собой CPython и все зависимости, которые у него есть.
Невозможно изменить адрес прокси (кнопка SETUP PROXY) Эта проблема наблюдается с клавиатурой MIUI и я никак не могу повлиять на нее. Единственный способ решения - выделить текст и начать вводить символы.
Аналоги
Заключение
Весь исходный код и APK вы можете найти на моем GitHub-е: https://github.com/GVCoder09/NoDPI4Android Там же находится подробная инструкция по сборке приложения в apk. Я искренне надеюсь, что эта программа принесет вам пользу, или, по крайней мере, заинтересует ее идея. Если вы хотите поддержать меня, то это можно сделать единственным способом - поставить плюсик статье :-)
Ну и конечно, я буду рад, если кто-то присоединится к разработке - issues и пул реквесты приветствуются :-)
