Салют, Хабр! На связи снова я, 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 и пул реквесты приветствуются :-)

Комментарии (10)


  1. Barnaby
    30.06.2025 16:25

    Единственным аналогом этой программы на Android явяляется... NoDPI

    А это что? https://github.com/romanvht/ByeDPIAndroid

    PS: По вашей же ссылке есть раздел "Похожие проекты", где много чего есть.