LxBox v2.25.2
L×Box v2.25.2
A patch on top of v2.25.1.
The main body of changes is in
v2.25.0 — one parsing
engine shared with
singbox-launcher 2.0.0,
and the Start insurance when the core refuses a server. Read those first;
this patch does not repeat them.
This patch is about subscriptions that used to come back short — and about not
being left in the dark when they do. Links providers actually write are read
instead of silently vanishing; a line the app cannot use says why. A decoy
banner from an expired subscription is no longer offered as a server, and a
transport the core does not speak is refused honestly instead of connecting to
nothing.
Патч поверх v2.25.1.
Основной корпус изменений — в
v2.25.0: один движок
разбора с
лаунчером 2.0.0
и страховка кнопки Start, когда ядро отказывается от сервера. Сначала
читайте их — этот патч их не повторяет.
Этот патч про подписки, которые приезжали короче, чем есть, — и про то, чтобы
не оставлять в тишине, когда так вышло. Ссылки в том написании, в котором их
пишут провайдеры, читаются, а не исчезают молча; строка, которую приложение не
может использовать, называет причину. Баннер-обманка истёкшей подписки больше
не выдаётся за сервер, а транспорт, которого ядро не знает, честно
отбраковывается вместо соединения в никуда.
🇬🇧 English
🩹 Fixes
Subscriptions and links
amneziawg://links no longer disappear. Panels spell the AmneziaWG
scheme out in full, and lines written that way used to vanish whole — every
field in them was already readable, but the list of known schemes lived in the
code as literals and did not include the long spelling.- A
vpn://link holding a plain WireGuard config gives a server. Under the
wrapper there may be not only an Amnezia profile but thewg-quick/
AmneziaWG config itself. Such a link used to yield zero servers with a message
about zlib that sent you looking for a fault that was not there. - A subscription that is a single Xray configuration — one object rather
than a list — is read as one server instead of yielding nothing. - Hysteria2 keeps its bandwidth and its Salamander obfuscation. A speed
written as a string with a unit ("100mbps") was dropped without a word, and
the obfuscation was lost together with its password, so the server arrived and
would not come up. Theup/downspelling the official client uses now
arrives too. - A provider panel's decoy banner is no longer taken for a server. When a
subscription has expired, panels return not an empty body but a
syntactically valid link to nowhere (0.0.0.0:1,127.0.0.1:1080) with the
explanation in the remark after#— and when the traffic quota is used up,
that entry can be the only one. What is judged now is the destination: an
entry whose address cannot belong to a server stays out of the list, and the
provider's own text reaches you as the reason. - A transport the core does not speak is refused instead of being swapped
out.network: kcp/quicused to reach the core verbatim, the sanitiser
stripped the transport silently, and the server came out as working plain TCP
— a server expecting mKCP will not accept that connection, and you saw no
reason why. - TCP header obfuscation (
headerType=http) refuses the server. It used to
be carried over into thehttptransport, which for the core means HTTP/2 —
a different protocol on the wire: a server expecting camouflage received an
h2 handshake and dropped the connection. The server looked healthy and did not
work. Realhttptransport (spelled out astype=http) is unaffected. - A socks password is no longer lost. v2rayN always writes a socks link as
base64("user:pass"), and parsing split the string on a:that is not there:
the name became the whole base64 string and the password vanished silently. wireguard,socksandhttpelements inside an Xray configuration are no
longer dropped. No section recognised them and the server disappeared
entirely, even though the core has every field they need.- ALPN from an Xray configuration reaches the server. It is part of the
handshake: a server with nothing to pick from what was offered drops the
connection, so a node facing an h2-only server simply did not work. - A number in
alpnorserver_portsno longer takes down the whole
configuration. A node from sing-box JSON with"alpn": [443, "h2"]reached
the core as written and the core refused to start at all. A non-string element
is now removed with a warning on that server, and its valid neighbours stay. - "No servers found" no longer keeps the reason to itself. A subscription
line whose protocol the app does not know, a link over the allowed length, and
a body that could not be read at all now each name their reason in the
notifications list, with the protocol spelled out instead of an empty list.
An unknown scheme and an unreadable body no longer both report themselves as
"protocol" — each has its own reason now. - Service lines that provider panels add are skipped quietly. Routing
commands addressed to neighbouring clients (incy://routing/…,
happ://routing/…) are not servers: a healthy subscription used to show five
refusals alongside working servers.
Workspaces
- Switching a workspace no longer overwrites the subscriptions in all of
them. If a subscription refresh was in flight at the moment of the switch —
by hand from ⟳, on the hourly timer, on return from the background, or after
the VPN was turned off — the outgoing workspace's screen would write its own
subscriptions into the workspace that had just loaded. Every workspace was
left with a single subscription, the last one refreshed, and the loss was
committed to disk. A refresh is now halted before the switch, and a write from
the outgoing workspace is rejected. Workspaces already overwritten are not
restored by this fix — only a backup can do that.
Start insurance
- Servers with the same name no longer cut the run short. Two unusable
servers sharing a name were being switched off one per press: after the first
went off its name passed to the second, the state machine read that as "the
same server again", and the VPN never came up. A repeat is now recognised by
the server itself, not by its name. - Stop during a server check no longer brings the VPN back. A stop that did
not come from the main screen's button — Debug API, the Quick Settings tile,
the Intent API, Tasker/Locale — left the check running, and a few seconds
later the insurance raised the tunnel by itself. Any Stop now cancels it. - The "disabled by insurance" list opens the server you tapped. When several
servers shared a refusal reason, or two servers in a folder shared a name,
tapping a row could open a different server's screen.
Servers screen
- Deleting a row no longer shifts its neighbours. From a list of
u1, c1, u2, c2you would delete the chainc1and getu1, c2, u2: a
record of the same kind slid into the freed slot and jumped over a server.
Slots are now matched by key, not by position. - Drag works when the storage holds a record the app cannot read. Any drag
used to roll back silently — the row jumped home. The visible records now
reorder, and the unreadable one keeps its slot. - A new row at the end of a long list is no longer hidden under the
SnackBar. A new record is appended at the tail, and the tail only scrolled
as far as the bottom padding, so the "New" row was covered by the
config-rebuild message. The list now keeps room below. - The highlight on a new row is dropped when the row is deleted. Deleting
within the seven seconds after adding — from the menu, or on a subscription
refresh — left the highlight bound to a dead record, and the screen kept
accumulating keys of deleted rows until it was closed. - LX Backup: importing keeps the source order from the file. A hop chain
sitting between servers in the file moved to the head of the list after
import, because chains and sources were written separately. New records now
take the file's order.
Config editor and DNS
- Config editor: a selection no longer collapses when the menu appears. A
long tap on the text opened the menu through a modal route, which took focus
away from the editor: the selection collapsed to a caret, and Copy put the
line under the caret into the clipboard instead of the fragment you had
selected. The menu now lives in an overlay bound to the editor — the selection
survives while the menu is on screen, and Cut / Copy / Paste / Select all work
on the real range. Both screens with an editor are covered: the shared config
and the add-server wizard. - The
dns_shieldDNS preset no longer resolves in the clear. The group was
set tomode: fastest— the query goes to every member at once, and plain UDP
with no TLS handshake almost always wins the race against DoH/DoT: the
"shield" regularly answered from an open resolver, and the UDP query was
visible to an observer even when an encrypted member won.google_udp,
cloudflare_udp,opendns_udpandyandex_udpare out of the group; the
first three already had an encrypted twin there, and anopendns_dohentry
was added for OpenDNS. The*_udpservers themselves stay in the list —
hints and resolver defaults point at them and you can still pick them.
Existing configurations are not rewritten automatically: the new composition
applies to fresh installs and on the next config build.
Stability
- "Check all servers" no longer crashes the app on naive servers. The check
gathered every server in the list into one temporary configuration and raised
it in one go. For naive that is disproportionately expensive: the core builds
a full Chromium network stack for each such server, in the same process as the
interface — a list with several naive servers hit the core's memory ceiling
and the process was killed, which from outside looked like the app crashing.
Naive servers are now checked in batches, one per configuration: the session
is shut down after each batch and the engines are freed before the next. Other
protocols are checked as before, in a single configuration; the order and
completeness of the check are unchanged.
🔧 Under the hood
Contract mirror 1.1.49 → 1.1.52: six parsing norms became executable, and the
scheme set is now taken from the contract registry rather than from literals in
the code — so a spelling that arrives with the contract works without a code
change. A manually chosen member of an imported selector group survives the
import → backup → import round trip, and an unread key inside settings /
streamSettings now gets a note instead of vanishing without a trace. The
socks4 link form changed to userid:@host — version 4 has no password by
protocol, but clients always write the separator, and some of them read its
absence as "no name".
🧪 Tests
CI checks (analyze, the full test suite, four l10n checkers, docs parity) is
the release gate. This patch's additions: parsing without a silent loss on any
path, seven regression tests on the workspace switch, the insurance identity
tests, mixed sources[] order on import, and the widget test for the new-row
highlight.
📚 Documentation
The diagnostics and Debug API pages, the guard list and the architecture notes
were brought back in line with the code after the v2.25.1 review. Google Play's
"edge-to-edge display" recommendation was traced to its source: the app makes no
calls to the APIs disabled on Android 15 — the references the static scanner
finds come from the Flutter embedding, where they already sit behind an
SDK_INT < 35 runtime gate. No code changed.
🇷🇺 Русский
🩹 Исправления
Подписки и ссылки
- Ссылки
amneziawg://больше не теряются. Панели пишут полное имя схемы
AmneziaWG, и такие строки исчезали целиком: все их поля приложение читать
умело, но список схем лежал в коде литералами и полного написания не знал. vpn://с голым.confвнутри даёт узел. Под обёрткой бывает не только
профиль Amnezia, но и сам конфигwg-quick/ AmneziaWG — прежде такая ссылка
давала ноль узлов с сообщением про zlib, которое уводило искать
несуществующую поломку.- Подписка из одного конфига Xray — одного объекта, а не списка — читается
как один сервер, а не даёт ноль. - Hysteria2 сохраняет полосу и обфускацию Salamander. Скорость, записанную
строкой с единицей ("100mbps"), узел терял молча, а обфускацию вместе с
паролём — целиком, из-за чего узел приезжал и не поднимался. Написание
up/downофициального клиента теперь тоже доезжает. - Баннер-обманка панели провайдера больше не считается сервером. При
истёкшей подписке панели отдают не пустое тело, а синтаксически валидную
ссылку в никуда (0.0.0.0:1,127.0.0.1:1080) и кладут объяснение в ремарку
после#; при исчерпанном трафике такая запись бывает единственной. Теперь
судится цель: запись с адресом, который сервером не бывает, в список не идёт,
а текст провайдера доезжает до человека причиной. - Узел с транспортом, которого ядро не знает, отбраковывается вместо
подмены.network: kcp/quicуезжал в ядро дословно, санитайзер снимал
транспорт молча, и узел выходил рабочим plain-TCP — сервер, который ждёт
mKCP, такое соединение не примет, а причины человек не видел. - Обфускация TCP-заголовком (
headerType=http) отбраковывает узел. Прежде
она переносилась в транспортhttp, а у ядра это HTTP/2 — на проводе другой
протокол: сервер, ждущий камуфляж, получал h2-рукопожатие и обрывал
соединение. Узел выглядел рабочим и не работал. Настоящий транспортhttp
(type=httpпрямым текстом) не затронут. - Пароль socks больше не теряется. v2rayN пишет socks-ссылку как
base64("user:pass")всегда, а разбор резал строку по:, которого в ней
нет: имя становилось всей base64-строкой, пароль исчезал молча. - Элементы
wireguard,socksиhttpвнутри конфига Xray больше не
пропадают. Ни одна секция их не опознавала, и узел исчезал целиком — при
том, что все нужные поля у ядра есть. - ALPN из конфига Xray доезжает до узла. Он есть часть рукопожатия: сервер,
которому нечего выбрать из предложенного, соединение обрывает, то есть узел с
h2-only сервером просто не работал. - Число в
alpnилиserver_portsбольше не роняет весь конфиг. Узел из
sing-box JSON с"alpn": [443, "h2"]уезжал в ядро как есть, и ядро
отказывалось стартовать целиком. Нестроковый элемент теперь снимается с
предупреждением на узле, годные соседи остаются. - «Серверов не найдено» больше не молчит о причине. Строка подписки, чей
протокол приложение не знает, ссылка сверх допустимой длины и тело, которое не
удалось разобрать вовсе, теперь называют причину в списке уведомлений — с
именем протокола, а не пустым списком. Незнакомая схема и нераспознанное тело
больше не сообщают о себе одним словом «протокол»: у каждого своя причина. - Служебные строки панелей пропускаются молча. Команды маршрутизации
соседним клиентам (incy://routing/…,happ://routing/…) узлами не
являются: у исправной подписки человек читал пять отказов при живых узлах.
Workspaces
- Переключение пространства больше не подменяет подписки во всех
пространствах. Если в момент переключения шло обновление подписок — вручную
по ⟳, по часовому таймеру, на возврате из фона или после отключения VPN, —
экран прежнего пространства успевал записать свои подписки в уже загруженное
новое. Во всех пространствах оставалась одна подписка, последняя обновлённая,
и потеря закреплялась на диске. Обновление теперь прерывается до
переключения, а запись от прежнего пространства отклоняется. Уже
перезаписанные пространства фикс не восстанавливает — только бэкап.
Страховка Start
- Узлы-тёзки больше не обрывают прогон. Два негодных сервера с одним именем
выключались по одному за нажатие: после выключения первого имя доставалось
второму, автомат принимал его за «тот же сервер повторно», и VPN не
поднимался. Повтор теперь опознаётся по самому серверу, а не по имени. - Stop во время проверки серверов больше не поднимает VPN обратно.
Остановка не кнопкой на главном экране — Debug API, плитка Quick Settings,
Intent API, Tasker/Locale — не отменяла идущую проверку, и через несколько
секунд страховка сама запускала туннель. Теперь любой Stop её отменяет. - Лист «выключено страховкой» открывает тот сервер, по которому тапнули.
При одинаковой причине отказа у нескольких серверов или у одноимённых
серверов в папке тап по строке открывал экран другого сервера.
Экран Servers
- Удаление записи больше не сдвигает соседей. Из списка
u1, c1, u2, c2
удаляли цепочкуc1и получалиu1, c2, u2: запись того же рода съезжала в
освободившийся слот и перепрыгивала сервер. Слоты теперь сопоставляются по
ключу, а не по позиции. - Перетаскивание работает при нечитаемой записи в хранилище. Любой drag
молча откатывался — строка прыгала на старое место. Теперь видимые записи
переставляются, нечитаемая остаётся в своём слоте. - Новая запись в конце длинного списка больше не под SnackBar. Запись
добавляется в хвост, а хвост прокручивался только до нижнего отступа — строку
с меткой «New» закрывало сообщение о пересборке конфига. Теперь у списка есть
запас снизу. - Подсветка новой записи снимается, если запись удалили. Удаление в течение
семи секунд после добавления (из меню или при обновлении подписки) оставляло
подсветку привязанной к мёртвой записи, а экран копил ключи удалённых строк до
закрытия. - LX Backup: импорт сохраняет порядок источников из файла. Цепочка,
стоявшая в файле между серверами, после импорта уезжала в голову списка:
цепочки и источники записывались по отдельности. Новые записи теперь встают в
порядке файла.
Редактор конфига и DNS
- Редактор конфига: выделение больше не слетает при показе меню. Долгий тап
по тексту открывал меню через модальный маршрут — тот забирал у редактора
фокус, выделение схлопывалось в каретку, и Copy уносил в буфер не выделенный
фрагмент, а строку под кареткой. Меню переехало в оверлей, привязанный к
редактору: выделение живёт, пока меню на экране, а Cut / Copy / Paste /
Select all работают с настоящим диапазоном. Затронуты оба экрана с
редактором — общий конфиг и мастер добавления сервера. - Пресет DNS
dns_shieldбольше не резолвит открытым текстом. В группе
стоялmode: fastest— запрос уходит всем членам сразу, а открытый UDP без
TLS-рукопожатия почти всегда выигрывает гонку у DoH/DoT: «щит» регулярно
отвечал из открытого резолвера, и запрос по UDP всё равно был виден
наблюдателю, даже когда побеждал шифрованный член. Из группы убраны
google_udp,cloudflare_udp,opendns_udpиyandex_udp; у первых трёх
шифрованный двойник в группе уже был, для OpenDNS добавлена запись
opendns_doh. Сами серверы*_udpостались в списке — на них ссылаются
подсказки и дефолты резолверов, выбрать их по-прежнему можно. Существующие
конфиги автоматически не переписываются: новый состав применяется к новым
установкам и при следующей сборке конфига.
Стабильность
- «Проверка на всех серверах» больше не роняет приложение на naive-узлах.
Проверка собирала все узлы списка в один временный конфиг и поднимала его
разом. У naive это несоразмерно дорого: ядро создаёт полный сетевой стек
Chromium на каждый такой узел, в том же процессе, что интерфейс, — список с
несколькими naive-узлами упирался в лимит памяти ядра и процесс убивало,
снаружи это выглядело как падение приложения. Теперь naive-узлы проверяются
порциями, по одному на конфиг: сессия гасится после каждой порции и движки
освобождаются до следующей. Остальные протоколы проверяются как прежде, одним
конфигом; порядок и полнота проверки не изменились.
🔧 Под капотом
Зеркало контракта 1.1.49 → 1.1.52: шесть норм разбора стали исполняемыми, а
набор схем берётся из реестра контракта, а не из литералов в коде — поэтому
написание, приехавшее контрактом, работает без правки кода. Выбранный вручную
член импортированной группы selector доживает круг «импорт → бэкап → импорт»,
а непрочитанный ключ внутри settings / streamSettings получает ноту вместо
исчезновения без следа. Вид ссылки socks4 сменился на userid:@host: пароля у
версии 4 нет по протоколу, но разделитель клиенты пишут всегда, и его
отсутствие часть из них читает как «имени нет».
🧪 Тесты
Релизный гейт — CI checks (analyze, полный набор тестов, четыре l10n-чекера,
паритет доков). Добавленное этим патчем: разбор без молчаливой потери на любом
пути, семь регресс-тестов на переключение пространства, тесты идентичности в
страховке, смешанный порядок sources[] при импорте и виджет-тест подсветки
новой записи.
📚 Документация
Страницы диагностики и Debug API, список гардов и заметки по архитектуре
приведены в соответствие с кодом после ревью v2.25.1. Рекомендация Google Play
«Отображение от края до края» разобрана до причины: своих вызовов отключённых на
Android 15 API у приложения нет — ссылки, которые находит статический сканер,
приходят из эмбеддинга Flutter, и там они уже закрыты рантайм-гейтом
SDK_INT < 35. Кода не меняли.
Install / Установка
adb install -r LxBox-v2.25.2-arm64-v8a.apkБез uninstall! Поверх существующей установки. Настройки и подписки сохранятся.
No uninstall needed — install over the existing one. Settings and subscriptions
are preserved.
Previous release / Предыдущий релиз: v2.25.1.
The main body of changes is in / Основной корпус изменений —
v2.25.0.