Привет. Полгода назад я выложил singbox-launcher — десктопный GUI для управления ядром sing-box. По фидбеку стало понятно: идея зашла, людям удобно собирать и отлаживать конфиги на ноутбуке, а потом переносить их на роутеры и другие сетевые устройства. Подробнее о десктопной версии я писал на Хабре.
Пару слов для тех, кто не в контексте. Есть небольшой класс кроссплатформенных сетевых ядер, которые умеют гибкую маршрутизацию трафика и поддерживают современный набор протоколов: WireGuard, VLESS, SOCKS5, Shadowsocks, Hysteria2, TUIC и так далее. Sing-box в этом списке — не самый раскрученный, но для меня он оказался наиболее интересным: быстрая эволюция, внимание к деталям, чистый код, живое общение мейнтейнеров с пользователями, классно организованный по логике конфиг.
Довольно быстро стали приходить запросы на Android-порт. Первое время казалось, что это будет прямое переиспользование десктопного кода. На практике сценарии потребления на мобильных оказались сильно другими: другой UX, другой lifecycle, Doze и background-лимиты, OEM-специфика, ограниченный экран, другие ожидания от автозапуска и обновлений. В итоге пришлось переписать практически всё с нуля.
Результат этой работы — LxBox, и сегодня я хочу рассказать, чем он отличается от существующих Android-клиентов и как устроен изнутри.

и всё, что называют «подпиской» или «конфигом»
Первое, что раздражает в существующих клиентах — каждому нужен «свой» формат. У кого-то только v2rayN base64, у кого-то — только собственный хитрый JSON. LxBox принимает всё, с чем приходится иметь дело на практике:
Формат |
Пример входа |
|---|---|
Подписка по URL |
|
v2rayN base64 |
base64-список |
Plain text |
построчный список URI |
Direct URI |
|
WireGuard INI |
стандартный |
Xray JSON Array |
полноценный xray-конфиг с |
Sing-box JSON outbound |
сырой outbound прямо из документации sing-box |
Clash/Clash-Meta подписки |
читаются как subscription URL |
Всё, что вы скопировали из чата, файла, QR-кода или спеки — LxBox попытается распознать. Формат детектится автоматически: «умный paste» анализирует тело, показывает превью того, что оно собирается сделать, и только потом добавляет.
Что мы делаем с HTTP-заголовками подписки
Кроме самого тела, правильный клиент обязан парсить HTTP-хедеры (de facto стандарт ещё с v2rayN 2018):
subscription-userinfo: upload=…; download=…; total=…; expire=…— текущая квота и срок;profile-title(опционально base64-encoded UTF-8) — человеческое имя подписки;profile-update-interval: 24— подсказка, как часто провайдер рекомендует обновляться;support-url,profile-web-page-url— ссылки на поддержку и сайт;content-disposition: attachment; filename="…"(RFC 5987) — fallback для имени.
Подписка отображается строкой вида:
124 nodes · ? 24h · ? 3h ago · (2 fails)
Где ? 24h — интервал обновления (из хедера или выбранный вручную: 1/3/6/12/24/48/72/168 ч), ? 3h ago — давность последнего успеха, в скобках — число неудач подряд.
Поддерживаемые протоколы
Протокол |
URI |
Транспорты |
|---|---|---|
VLESS |
|
TCP, WS, gRPC, H2, HTTPUpgrade, REALITY |
VMess |
|
TCP, WS, gRPC, H2, HTTPUpgrade |
Trojan |
|
TCP, WS, gRPC |
Shadowsocks |
|
TCP, UDP, SIP003 плагины |
Hysteria2 |
|
QUIC, Salamander obfs, port-hopping |
TUIC v5 |
|
QUIC, BBR/CUBIC/NewReno, 0-RTT |
SSH |
|
TCP, host key / password / private key |
SOCKS5 |
|
TCP, auth |
WireGuard |
|
UDP, multi-peer |
Подробнее со всеми параметрами каждого URI и соответствием sing-box outbound'ам — в docs/PROTOCOLS.md репо.
2. Подписки, которые не спамят провайдеру и не падают
Что отличает LxBox о других решений в этом вопросе это бережное обращение с сетью и то, что rebuild конфига никогда не ходит в сеть. Любая перегенерация sing-box JSON — это чистая локальная сборка из уже загруженных нод. Я видел клиенты, где каждое открытие настроек = HTTP-запрос по подписке и если сети нет, то и конфига нет. У нас не так, если уже смогли хоть раз считать — будем пользоваться этим.
Детали — в spec 027 subscription auto update.
3. Статистика и наблюдаемость на нормальном уровне
Это моя любимая часть. В типовом клиенте вы видите две цифры — upload/download. В LxBox я сильно расширил функционал.
Экран Statistics
Сводка: общий upload, общий download, число активных соединений.
Трафик сгруппирован по outbound'ам (vpn-1, auto-proxy-out, direct-out, конкретные ноды). Каждая группа — раскрывающаяся карточка.
Для нод с detour (см. ниже) в шапке карточки рендерится цепочка ступеньками:
?? Литва-bypass ↑ 102.0 KB ↳ via ⚙ socks 45.142.73.159 ↓ 299.7 KB 4 connections ⌄
Вот как это выглядит на живом устройстве — вкладка Overview: сводка, трафик по outbound'ам (здесь Нидерланды и direct-out), группировка по routing-правилам и Top apps:
Экран Statistics — Overview
там же экран Connections:
Все активные соединения, сортировка по времени.
Для каждого: хост:порт, протокол (TCP/UDP), применённое правило маршрутизации, цепочка прокси, длительность, upload/download, имя приложения/процесса.
Можно закрыть отдельное соединение крестиком (DELETE
/connections/{id}).«Close all» в AppBar — обрубить всё разом.
Auto-refresh каждые 2 секунды.
Когда что-то «не так» (сайт не грузится, приложение ходит мимо правила, трафик утекает в direct) — обычно сразу видно, где именно.
Дополнительно из полезного:
Встроенный speed test на 10 серверах в разных городах (Cloudflare, Hostkey × 5, Selectel, Tele2, OVH, ThinkBroadband), настраиваемое число параллельных потоков (1/4/10), история сессий.
Mass-ping — 20 параллельных пингов по всем нодам с цветовой раскраской по латенси.
Heartbeat туннеля — каждые 20 секунд опрашиваем Clash API, два подряд фейла = считаем туннель мёртвым. Ловит перехват другим VPN-приложением или тихую смерть сервиса. и все что выше названо пингом — это HTTP запросы а не imap так что все будет четко работать
в общем вам надо это попробовать и поймете почему это круто.
4. Неочевидные настройки
Android-мир жестокий: Doze, App Standby, background-лимиты, OEM-специфика (Xiaomi, Samsung, Oppo, Huawei — у всех свои тоглы). Тут реализовано очень бережное обращение с батарейкой. Много, очень много тестирования.
Что мы умеем:
Tunnel sleep mode — три режима, на выбор
Настройка Settings → Background → Tunnel sleep mode:
never(дефолт) — туннель активен всегда. Стабильные push-уведомления и SIP/VoIP. Цена — +1–3% батареи за ночь.lazy— pause только при глубоком Doze (это дефолт у других решений и часто у пользователей ломались long-lived TCP и push').always— pause на каждое выключение экрана. Максимум экономии, но push будут отваливаться.
Еще полезного:
При старте приложение проверяет
isIgnoringBatteryOptimizations. Если не whitelisted — показывает диалог с переходом в системные настройки (rate-limited: не чаще раза в сутки).Отдельная плитка
App info (OEM power settings)открывает системныйSettings.ACTION_APPLICATION_DETAILS_SETTINGS+ перед этим показывает инструкцию: «ищите в следующем экране тоглы Autostart / Background activity / Battery saver exceptions». У Xiaomi/MIUI, Samsung, Oppo/ColorOS и Huawei они называются по-разному и спрятаны в разных местах — без этой подсказки обычный пользователь их не находит.Auto-start on boot — Стандартный
RECEIVE_BOOT_COMPLETED+BootReceiver. Если пользователь так настроил — VPN поднимается сам после перезагрузки. — Keep VPN on exit: должен ли VPN гаситься при swipe'е приложения из recents или держаться как независимый foreground-service. Реализовано черезonTaskRemoved+ структурированныйserviceScope. Поддерживает сценарий «Flutter-процесс умер, сервис живой»: когда пользователь возвращается, новый UI восстанавливает состояние туннеля через pull-sync (getVpnStatusвinit, не только броадкасты, которые могут быть потеряны в Doze).
5. Несколько VPN-каналов одновременно, а не один туннель
В большинстве мобильных клиентов модель простая: вы подключены к одному прокси, и правила маршрутизации решают только «через прокси или напрямую». В LxBox модель другая: одновременно могут работать несколько независимых каналов, и каждое правило явно выбирает, в какой из них ехать.
Во вкладке Routing → Channels (на коллаже выше — третий скрин) видно:
VPN ① (selector:
vpn-1) — основной канал. По умолчанию туда направляется весь трафик, не попавший под правила (route.final).VPN ② (selector:
vpn-2) — второй канал, например, для сервисов, которые должны видеть конкретную страну.VPN ③ (selector:
vpn-3) — третий.Include Auto (urltest:
✨auto) — автоматический селектор, который сам выбирает самую быструю ноду по Clash URLTest'у и сам перетекает между нодами по мере изменения латенси.
Каждый канал — это отдельный selector outbound в sing-box, в который можно положить любой набор нод из любой подписки. Один и тот же сервер может быть в нескольких каналах одновременно — это не копия, а ссылка на outbound, «дорого» от этого не становится.
Живой сценарий, которым сам пользуюсь:
Канал ① — основная подписка, быстрые европейские ноды. Туда уходит весь трафик по умолчанию.
Канал ② — мой домашний WireGuard. Туда я отправляю банковские приложения, чтобы они всегда видели один и тот же «домашний» IP.
Канал ③ — специальные региональные ноды (японские, американские — под конкретные сервисы, у которых геолокация завязана на IP).
Правило в Routing → Rules (четвёртый скрин на коллаже) указывает какой канал использовать — или direct (в обход туннеля), или reject (заблокировать)
Любое правило одним тапом на dropdown справа перенастраивается на нужный канал
Важно, что все активные каналы держат параллельные соединения. Когда одно приложение ходит через ①, другое — через ②, а третье — напрямую, это происходит одновременно, без переподключения туннеля. Эффект «умного VPN», где каждое приложение едет по своему маршруту, а не «один общий провод на всех».
В большинстве Android-клиентов такой конструкции либо вовсе нет (один глобальный прокси), либо она собирается руками в сыром sing-box JSON. В LxBox это first-class поверхность настройки.
6. Detour: собственный «первый хоп» для ненадёжных сетей
Объясняю максимально просто.
Представьте кафе с капризным Wi-Fi или провайдера, у которого сегодня что-то с роутингом до вашего обычного сервера в условной Германии. Связь до него то работает, то нет, туннель падает.
Detour-сервер — это промежуточный сервер, через который ваш трафик едет до основного. Классический сценарий:
Домашний роутер с WireGuard → весь трафик LxBox сначала прыгает на него, потом уходит на любой VLESS/Trojan/Hysteria.
Плюсы:
Первый хоп — всегда «к себе», он стабилен.
Дальше уже работает качественный канал между двумя серверами (не обязательно вашим мобильным интернетом).
Если провайдер глючит именно до конкретного зарубежного IP — ваш домашний/рабочий роутер ходит до него гораздо более стабильно.
В LxBox это выделено как отдельный продуманый сценарий:
Любую ноду можно пометить как detour (переключатель
Mark as detour serverв настройках ноды, появляется префикс⚙).Override detour на уровне подписки — все ноды этой подписки поедут через выбранный detour. Один тогл — вся подписка перемаршрутизирована.
Register / Use / Register-in-Auto — тонкая настройка, как именно показывать detour-ы в списке нод и в auto-urltest.
Работает со всеми протоколами: ваш WireGuard-detour + VLESS-выходной узел, или SSH-detour + Shadowsocks — как захотите.
Для подписок, которые сами приходят с dialerProxy (Xray JSON-подписки с цепочками) — он распознаётся автоматически, внутренние серверы получают префикс ⚙ и корректно регистрируются как detour-кандидаты.
В большинстве других клиентов multi-hop цепочки либо вообще не поддерживаются, либо собираются руками в сыром JSON, так что этой фичей я тоже горжусь.
7. Как это устроено внутри — 3-слойный pipeline
Это раздел «для гиков», но без него сложно понять, почему LxBox ведёт себя именно так.
UI / Controller │ paste / URL / QR / file → SubscriptionSource ▼ parseFromSource(source) — слой 1: парсинг │ HTTP fetch + body_decoder + typed parser │ returns: List<NodeSpec>, meta, rawBody, headers ▼ ServerList (sealed) — слой 2: контейнер │ SubscriptionServers | UserServer │ .build(ctx: EmitContext): │ ├─ applies tagPrefix + allocateTag │ ├─ per-node emit(vars) → SingboxEntry │ ├─ applies detour policy (register/use/override) │ └─ registers в selector / auto-proxy-out groups ▼ buildConfig(lists, settings) — слой 3: сборка │ template (assets/wizard_template.json) + post-steps: │ 1. server_list_build outbounds/endpoints из ServerList │ 2. applyPresetBundles expansion CustomRule(kind: preset) → merge │ 3. applyCustomRules inline + local-SRS правила │ 4. flush registry → config.route.{rule_set, rules} │ 5. applyCustomDns dns.servers/rules из template + extras │ 6. validator dangling refs, empty urltest, etc. ▼ BuildResult { config, configJson, validation, emitWarnings } ▼ HomeController.saveParsedConfig(config) ▼ native VpnService (libbox)
Ключевые свойства этой архитектуры:
NodeSpec— sealed class на 9 вариантов. ОдинNodeSpec→ 1–2SingboxEntry(WireGuard едет вendpoints, остальные — вoutbounds). Любойswitchпо типу — безdefault, компилятор ловит забытую ветку.Round-trip инвариант:
parseUri(spec.toUri()) ≈ spec. Протестировано на всех 9 протоколах — можно туда-сюда без потерь.Immutability по умолчанию. Mutable — только
ServerList.nodes(перезаписывается на refresh) иNodeSpec.warnings(добавляются warning'и).EmitContext.allocateTag(baseTag)— глобальная уникальность тегов между всеми подписками. Коллизии → суффиксы-1,-2. Сервисные теги шаблона (direct-out,dns-out,block-out) преднаполнены в taken-set, пользовательские с ними не конфликтуют.Warnings bubble up: parse-time →
NodeSpec.warnings, emit-time → добавляется вemit(пример: XHTTP fallback — sing-box его не умеет, мы даунгрейдим до HTTPUpgrade и оранжевым баннером сообщаем «с этим узлом, скорее всего, не соединится»).Валидатор возвращает типизированный
ValidationResult { fatal[], warnings[] }: dangling refs, пустой urltest, невалидный selector default. Подробности — в UI, «почему конфиг не собирается» никогда не остаётся загадкой.
Wizard-template (assets/wizard_template.json) — единый источник баз: дефолтные DNS-серверы, preset-группы (VPN①/②/③ + @auto), каталог правил маршрутизации, vars. Пользовательский override лежит в lxbox_settings.json. Правило: если нужен новый default — добавляй в template, не хардкодь в Dart.
Полная схема дерева, потоков данных и native-слоя — в docs/ARCHITECTURE.md.
8. Отладка и Debug API
Эта часть чисто для тех, кто любит копаться.
У LxBox есть встроенный HTTP-сервер на 127.0.0.1:9269 — выключен по дефолту, включается тоглом App Settings → Developer → Debug API. Токен генерится при первом включении, показывается в UI с кнопкой Copy, нигде в файлы не пишется (единственный канал — ваш буфер обмена).
Через adb forward tcp:9269 tcp:9269 всё становится доступно с ноутбука.
Что умеет:
Чтение состояния:
GET /state(HomeState, tunnel, nodes, traffic),/state/subs,/state/rules,/state/storage(с маскированием чувствительных полей),/state/clash,/state/vpn,/device(версия Android, ABI, battery-opt, network type).Clash API proxy —
/clash/proxies,/clash/group/<tag>/delay,/clash/connectionsи т.д. Секрет подмешивается автоматически.Actions —
POST /action/ping-all,/ping-node,/run-urltest,/switch-node,/start-vpn,/stop-vpn,/rebuild-config,/refresh-subs,/download-srs,/toast(показать Android-toast — sanity-check «моё ли это устройство на adb»).CRUD на доменные ресурсы:
/rules/*(create/update/delete/reorder),/subs/*(add/patch/delete/refresh),/settings/*(scoped writes наroute_final,excluded_nodes, vars, DNS).PUT /config— прямой override saved sing-box JSON, минуя сборку. Для A/B тестов руками, без пересборки APK.GET /help?format=text|json— самодокументируемая карта API. Без auth (как/ping). Markdown для LLM-агентов, JSON для авто-тулинга. Любой MCP-wrapper или скрипт стартует с одного запроса и знает всё, что есть.
Безопасность:
Bind строго
127.0.0.1(не LAN).Host header check (anti DNS-rebinding) — запрос с
Host: evil.comрежется 403.Bearer token, Host-check срабатывает до auth.
Write-эндпоинты имеют явный allow-list ключей (вы не заблокируете сами себе доступ через API).
Звучит как overengineering для VPN-клиента — но это то, что превращает отладку сложного кейса из «пересоберу APK, перезалью, погляжу» в «один curl».
Полная спека — spec 031 debug api. В разработке — обёртка этого API в MCP-сервер (spec 035 mcp server), чтобы LLM-агент мог напрямую дёргать actions без промежуточного REST.
9. Spec-driven разработка — как это устроено в репозитории
Я серьёзно верю, что серьезные проекты не могут жить без письменной дисциплины и в эпоху агентного ПО это особенно стало актуальным. Я занимаюсь профессионально выстраиваением процессов разработки и хорошо вижу, что как только фич становится больше тридцати, невозможно удержать в голове, почему вот здесь именно так и какие инварианты поддерживает этот код. Поэтому в LxBox каждая фича сначала описывается, потом реализуется, только так можно актинво использовать агентов и показывать быстрое и динимичное развитие проектов.
Где это живёт:
docs/ spec/ features/ 001 mobile stack/spec.md 002 mvp scope/spec.md ... 026 parser v2/spec.md ← landmark-рефакторинг v1.3.0 027 subscription auto update/spec.md 030 custom routing rules/spec.md 031 debug api/spec.md 036 update check/spec.md tasks/ 001-reconnect-sink-leak.md 002-blocking-stopvpn-intent-reset.md ... ← журнал рабочих циклов processes/ night-work/ ← autonomous-сессии (AI-агент ночью) ARCHITECTURE.md ← 3-слойный pipeline, потоки данных DEVELOPMENT_GUIDE.md ← принципы, тесты, процесс релиза PROTOCOLS.md ← URI-форматы всех 9 протоколов releases/ v1.5.0.md ← per-version notes, EN + RU
Если хотите покопаться — начните с docs/spec/README.md, а дальше по интересу: 026 parser v2 для архитектуры, 027 subscription auto update для «как не спамить провайдеру», 031 debug api для «как выдернуть любое состояние наружу». Каждая спека — самодостаточный документ.
Попробовать
Репо: https://github.com/Leadaxe/LxBox — там APK в Releases, документация (EN + RU), все спеки, ARCHITECTURE.md, PROTOCOLS.md.
Issues и PR приветствуются. Больше всего ценны кейсы, где поведение разошлось с ожиданием — они двигают проект в правильную сторону сильнее любой «фичи».