Skip to content

LxBox v2.25.2

Choose a tag to compare

@eleutherifer eleutherifer released this 24 Sep 04:19
· 490 commits to main since this release
0feb6fd

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 the wg-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. The up / down spelling 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 / quic used 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 the http transport, 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. Real http transport (spelled out as type=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, socks and http elements 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 alpn or server_ports no 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, c2 you would delete the chain c1 and get u1, 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_shield DNS preset no longer resolves in the clear. The group was
    set to mode: 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_udp and yandex_udp are out of the group; the
    first three already had an encrypted twin there, and an opendns_doh entry
    was added for OpenDNS. The *_udp servers 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.