От десктопа к Android и в роутеры
- пятница, 14 августа 2026 г. в 00:00:18
Всем привет, возможно, вы уже пользовались моими L×Box или Sing-box Launcher, я про них уже писал. Если нет, то коротко: на сегодня это уникальные VPN клиенты, которые предоставляют гибкую машрутизацию на множестве VPN соединений одновременно, поддерживают сложные сценарии туннелирования, адаптированы прод экстремальные сценарии использования и обладают широкой диагностикой.
Я сегодня расскажу про развитие ядра этой экосистемы (первая часть тут)
Ядро тоже OpenSource https://github.com/Leadaxe/sing-box-lx/ и входит в 10ку самых популярных форков sing-box.
В прошлой статье я говорил про два протокола — XHTTP и AmneziaWG 2.0 — и про три правила, которые не дают форку сгнить: минимальное касание апстрима, регулярный ребейз, воспроизводимые сборки. Заканчивалась она обещанием «но это уже следующая история».
История получилась объёмнее, чем я планировал. С июня в репозитории больше 1700 коммитов, 24 стабильных тега линейки 1.14 и под семь десятков спек. Вам интересно, наверное, траектория развития и она такая: ядро, которое начиналось как движок для десктопного лаунчера, сначала обросло адаптациями на Android, а теперь едет на роутер, потому что адаптации одни и те же. Об этом и расскажу.
MASQUE. В апстримном sing-box этого протокола нет. У меня теперь есть клиентский outbound type: masque: CONNECT-IP по RFC 9484 — именно CONNECT-IP, а не CONNECT-UDP, который часто под этим словом подразумевают. Разница принципиальная: туннелируются целые IP-пакеты, поверх потока поднимается тот же userspace-сетевой стек, что у WireGuard-эндпоинта, и приложения получают полноценный туннель, а не проброс датаграмм. Главный профиль — Cloudflare WARP, но протокольная часть написана как общий CONNECT-IP, WARP — лишь пресет поверх. Регистрация устройства в WARP сознательно вынесена из ядра: это можете в клиентах смотреть, там есть.
AmneziaWG. В первой статье AWG 2.0 был «фичей форка»; за лето я его довел, до уровня, эталонной клиентской реализации. Тут есть методологическая засада: у AmneziaWG нет спецификации — ни RFC, ни whitepaper, протокол задан реализациями. Поэтому вместо «вроде всё сделали» я провёл формальную сверку по трём независимым первоисточникам: конфиг-парсер amneziawg-tools, netlink-контракт ядерного модуля и наш форк. Результат — паритет 16 из 16 параметров обфускации: junk-пакеты Jc/Jmin/Jmax, паддинги S1–S4, магические заголовки H1–H4 (включая диапазонные) и сигнатурные пакеты I1–I5 с их CPS-мини-языком — все восемь тегов, один в один с эталоном, сквозной цепочкой от конфига до устройства. Причём проверено живым прогоном IpcSet → IpcGet, а не грепом по исходникам — грепом такое как раз легко проглядеть. Добил вопрос AWG поверх WireGuard: вложенные туннели работают, включая AWG-over-AWG через detour —для этого пришлось разрешить фрагментацию внешней UDP-датаграммы, которую ядро по умолчанию запрещает.
Десктоп прощает почти всё. Телефон — вообще ничего.
Энергетика. Держать одновременно пару десятков WireGuard/AWG-туннелей на телефоне — это превратить телефон в грелку. Пришлось построить полноценную машину состояний: простаивающие эндпоинты усыпляются (вплоть до полного сноса userspace-устройства), просыпаются лениво при первом дайле, health-check'и научились пропускать циклы, если через узел только что успешно ходил реальный трафик. Результат — минус 31% RSS (Resident Set Size) на боевом конфиге и -80% нагрузки на ядро. Самое сложное здесь было— не усыпить, а понять, кого усыпить. Конфиг sing-box — это граф, обратно направленный: каждый узел знает, через кого он дайлит (detour), но никто не знает, кто дайлит через него. При этом граф ещё и динамический: selector — ручные переключения и urltest автоматические. Все это сделано: теперь сокеты закрываются пачкой, эндпоинты — конкурентно; стопы укладываются в доли секунды.
Сон и пробуждение. После doze телефон просыпается, пользователь первым делом меряет пинг — ну вы поняли: раньше задержка была до 90сек, теперь окно ошибок схлопнулось до одного RTT рукопожатия.
127 секунд тишины. Самый красивый из унаследованных багов: TCP-дайлы через userspace-netstack были единственным классом путей в ядре вообще без таймаута. Единственной границей оставался SYN-бэкофф gVisor: 1+2+4+8+16+32+64 — две с лишним минуты до ошибки, а для доменных назначений — умножить на число адресов. Исправил и это, не ввел ни одного нового таймера — все проверки считаются дельтами по меткам времени, потому что таймеры на Android переживают сон непредсказуемо.
Есть класс багов, которые я не люблю больше всего: узел мёртв, а в логе — ни строчки. За лето таких набралось на маленький сериал.
REALITY и три байта из 2023 года. Xray v26.7.11 включил проверку минимальной версии клиента по умолчанию. А апстримный sing-box объявляет в REALITY-рукопожатии версию 1.8.1, зашитую константой ещё в 2023-м. Самое коварное — при провале проверки сервер не рвёт соединение, а прозрачно проксирует его на камуфляжный сайт с настоящим сертификатом. Так и задумано, иначе цензор отличал бы REALITY-сервер по форме отказа. Для пользователя это «узел не работает, лог чист» — неотличимо от кривого short_id или разъехавшихся часов. Фикс — три байта.
XHTTP и косая черта. Сервер XHTTP нормализует свой путь до завершающего / и матчит запросы по префиксу. Наш stream-one слэш срезал: узел, настроенный как /api/v1/feed, дайлился ровно туда, сервер ждал /api/v1/feed/. Итог — 404, который наружу выглядел как вечное молчание, и все XHTTP-узлы подписки казались мёртвыми. Следом довёл packet-up (взаимная блокировка: Xray отдаёт downlink только после первого uplink-пакета, а мы не отдавали conn наверх, пока не придёт ответ на download — обе стороны вежливо ждали друг друга, а reverse-proxy добивал 504-м) и реализовал XMUX — переиспользование HTTP-соединений, которое приходит в конфигах подписок и до сих пор молча отбрасывалось.
Постквантовый VLESS. Узлы из подписки: WebSocket-апгрейд проходит со 101, gRPC отвечает на SETTINGS — и затем сервер молча рвёт соединение. Клиентскую половину encryption: mlkem768x25519plus сделал, серверную сознательно не взял — форк у меня клиентский.
1488 байт проходит, а 1502 байт исчезает :) как тебе такое? Связки вида «VLESS за WARP'ом» (masque → detour → VLESS) на части узлов висли 15 секунд и падали с tls handshake: EOF. Причина такая: второй сегмент соединения пересыл наш ClientHello от своего имени, и если PMTU за ним меньше размера ClientHello, пакет теряется молча — ICMP «fragmentation needed» до клиента не доходит. Лечение: фрагментация первой TLS-записи теперь включается сама, когда outbound дайлит через detour.
Большая часть уникального сетевого профайлера L×Box и Sing-box Launcher строилась на том, что ядро я последовательно учил быть диагностируемым. Это большая работа со своей историей (смотреть тут: OBSERVABILITY), и вот она оказалась супер важной для роутеров и серверов.

У ядра появилась третья жизнь: sing-box lxd — headless-демон, который хостит ядро внутри себя и выставляет долгоживущий канал управления. Целевой сценарий — роутер: ядро крутится например на OpenWrt, а лаунчер управляет им по сети.
Чем это отличается от классического sing-box run на роутере:
в sing-box же есть Clash API — зачем что-то ещё? Clash API — это REST-эмуляция интерфейса другого ядра, и всё, что в модель Clash не влезает, туда физически не пролезает. Суммарный трафик, список соединений, переключить селектор, померить задержку — да; а вот цепочки detour, структурный DNS-поток, подробности urltest-групп, apply конфига с валидацией и откатом — этих сущностей в Clash-схеме просто не существует. Весь OBSERVABILITY через эту щель не просунуть. Поэтому мой демон lxd говорит на gRPC-протоколе — том же, что CommandClient у Android-линейки: типизированные стримы вместо поллинга, полная модель данных ядра, и один код клиентской стороны на телефон и роутер.
Канал переживает reload. У run смена конфига — это рестарт процесса: клиент теряет соединение, стримы наблюдаемости рвутся. У демона слушающий порт принадлежит демону, а не box-инстансу: apply подменяет ядро «под» живым сервером.
Демон доступен именно тогда, когда данные не ходят. Канал управления поднимается до ядра: битый конфиг оставляет демон на связи — чинить конфиг можно по тому же каналу, который нужен ровно при лежащем data-plane.
Конфиг с гарантиями. POST /admin/apply валидирует кандидата собственным sing-box check, при провале старта откатывается на последний рабочий, помнит прерванный apply через рестарты и ребуты. Залить кривой конфиг на удалённый роутер и потерять его — невозможно by design.
Доверие без боли. mTLS, где демон — сам себе CA: клиент регистрируется одноразовым инвайтом вида адрес#отпечаток#код и дальше опознаётся по сертификату в обе стороны. Никаких паролей в конфигах лаунчера.
Один демон несёт две ключевые фишки: gRPC-наблюдаемость (тот же протокол, что у Android-линейки — статусы, логи, группы, DNS-поток, соединения, детали) и admin-REST. И на этом же порту, за тем же клиентским сертификатом, демон умеет рассказывать о себе и о сервере: свой лог, память процесса, трафик ядра, полный набор pprof-профилей, и диагностика состоний и интерфейсов. Апстримная альтернатива ужасна — experimental.debug.listen — открывает второй порт вообще без аутентификации (здесь выключена), у меня второго порта нет намеренно, все на одном порту за сертификатом.
Отдельная фича, которой я откровенно доволен, — справочник «IP → устройство». Сетевой инспектор на роутере показывал Client: 192.168.20.238:50558; для сети из пятнадцати устройств это ребус. Демон крутится на той стороне, которая имена и раздаёт, — поэтому ручка GET /admin/clients-info собирает карту из пяти источников, каждый следующий уточняет предыдущий: DHCP-лизы (шесть форматов уже разобраны ядром апстрима — свой парсер писать не пришлось) → ARP-таблица (закрывает статические IP) → bridge fdb (в какую физическую розетку воткнут проводной клиент) → hostapd через ubus (SSID и точный AP-интерфейс) → метки оператора. Теперь соединение подписано именем устройства, а в каждой записи есть поле source — когда устройство теряет имя, сразу ясно какой источник замолчал и что докрутить.
Туда же — доставка файловых ресурсов конфига (.srs-рулсеты) по REST с sha256 для диффа, и защита целостности: удалить ресурс, на который ссылается активный конфиг, нельзя.
Прямо сейчас в работе телеметрия самого хоста — CPU, память, температура, диски, дескрипторы для легкой диагностики.
Практическая сторона уже тоже закрыта: релизы собирают статические musl-бинари под роутерные архитектуры (включая big-endian MIPS для старых Atheros).
В доках лежит пошаговый рецепт отдельного VPN-SSID на OpenWrt: гостевой Wi-Fi, весь трафик которого уходит через ядро, не трогая основную домашнюю сеть:
https://github.com/Leadaxe/sing-box-lx/blob/lx/docs-lx/openwrt-vpn-ssid-ru.md
Три правила из первой статьи выжили, и оказались ключевыми:
Дрейф от апстрима меряется по merge-base, а не по счётчику коммитов. и это на каждый релиз. Все всегда актуально.
Сабмодули разбираются до мержа ядра. Все подсистемы проверяются целостно. Кстати благодаря этому смог сделать полную сборку всего из исходников для F-Droid.
Стабильный тег не срезается без прогона на реальном устройстве. Эмулятор не ловит ни вендорные ядра, ни настоящий doze. Всё, что проверено только стендом, честно помечено в релиз-нотах: «полевого прогона не было».
И каждый релиз — с рукописными машинописными (конечно) билингвальными нотами.
Демон только начал жить на роутерах — впереди телеметрия хоста, полевой прогон штрафной механики urltest и дальнейшее сращивание с лаунчером и L×Box. Если у вас есть свежий сервер Xray с REALITY (≥ v26.7.11) — отчёты особенно ценны: своих стендов под все сценарии не хватает.
Как обычно: ядро открыто — https://github.com/Leadaxe/sing-box-lx/. Баги — в issues, лучше с крашдампом или конфигом; звёздочки приветствуются; спеки в SPECS/ читаются как отдельный жанр, если вам интересна эта кухня. Спасибо всем, кто присылал полевые репорты и пользуется этими решениями. Мой форк уже вошёл в десятку самых популярных:
