github Leadaxe/singbox-launcher v1.5.1
release v1.5.1

3 hours ago

Release v1.5.1

Downloads

macOS (Universal) - Supports both Apple Silicon and Intel

Option 1: Installation Script (Recommended)

Install with a single command (version v1.5.1):

curl -fsSL https://raw.githubusercontent.com/Leadaxe/singbox-launcher/develop/scripts/install-macos.sh | bash -s -- v1.5.1

The script will:

  • Download the release archive
  • Extract and install to /Applications/
  • Fix macOS quarantine attributes and permissions
  • Launch the application automatically

Option 2: Manual Installation

  1. Download: singbox-launcher-v1.5.1-macos.zip
  2. Extract the ZIP file
  3. Remove quarantine attribute (required):
    xattr -cr "singbox-launcher.app" && chmod +x "singbox-launcher.app/Contents/MacOS/singbox-launcher"
  4. Double-click singbox-launcher.app to run
    • If macOS blocks the app, go to System Settings → Privacy & Security and click "Open Anyway"
    • Alternatively, right-click the app and select "Open" (first time only)

Windows (amd64)

  1. Download: singbox-launcher-v1.5.1-win64.zip
  2. Extract the ZIP file to a folder, for example: C:\Program Files\singbox-launcher\
  3. Run singbox-launcher.exe from that folder
    • You may need administrator rights to install to Program Files
    • The launcher will automatically download sing-box and wintun.dll on first launch

Windows 7 (x86, legacy)

  1. Download: singbox-launcher-v1.5.1-win7-32.zip
  2. Extract the ZIP file to a folder and run singbox-launcher-win7-32.exe
    • For Windows 7 / 32-bit or legacy compatibility only

Linux Support

⚠️ Linux build temporarily unavailable - мы ищем тестировщика для ручного тестирования перед включением автоматической сборки.

Checksums

See checksums.txt for SHA256 checksums of all files.

v1.5.1

Hotfix release: two defects that could hit anyone updating to or running
v1.5.0 — an infinite subscription-refresh loop and a macOS install script
that wiped user data on update.

EN

Highlights

  • Fixed an infinite subscription-refresh loop. If any enabled
    subscription failed to fetch persistently (device limit reached, expired
    token) while its local cache was missing, the launcher fell into a
    mutual recursion between the refresh and rebuild pipelines: it re-fetched
    every subscription about once a second, the progress bar bounced between
    35% and 70% forever, and config.json was never written. Now a source
    with no cache degrades with a warning — the config builds from the
    remaining subscriptions — and the internal refresh triggered by a rebuild
    no longer triggers another rebuild.
  • The macOS install script no longer wipes user data on update.
    install-macos.sh (0.2) preserved only wizard states; everything else
    went to Trash with the old bundle: settings.json (device id — so
    device-bound subscriptions saw a "new device", language, core engine
    mode, debug API), the daemon pairing certificate ("client certificate
    not trusted" after update), remote machines (the Remote tab emptied),
    and subscription caches (which is exactly what armed the refresh loop
    above). The script (0.3) now backs up the whole data directory and
    restores it without overwriting files that ship in the new archive.

Technical / Internal

  • buildSnapshotFromRawCache returns ErrRawCacheIncomplete (→ auto-update
    fallback) only when no enabled subscription has a cache; a partially
    missing cache degrades the affected source with a warning instead of
    blocking the build. Regression test included; the missing-source URL in
    the test points at 127.0.0.1:1 so it never depends on external network.
  • UpdateConfigFromSubscriptions got an internal triggerRebuild flag:
    the tail rebuild is skipped when the update was itself started by
    RebuildConfigIfDirty's cache-fallback — that edge was the recursion.
  • The command dialog (daemon kickstart after a core update, Linux
    capabilities) renders its message and command in the normal foreground
    color and sizes to content — previously both were drawn in the disabled
    placeholder color, nearly invisible on the dark theme (shipped in the
    v1.5.0 tag, listed here for completeness).

RU

Основное

  • Починен бесконечный цикл обновления подписок. Если у включённой
    подписки fetch падал стабильно (лимит устройств, протухший токен), а
    локального кэша у неё не было, лаунчер попадал во взаимную рекурсию
    конвейеров обновления и пересборки: перефетчивал все подписки примерно
    раз в секунду, прогресс вечно скакал между 35% и 70%, а config.json
    так и не записывался. Теперь источник без кэша деградирует с
    предупреждением — конфиг собирается из остальных подписок, — а
    внутреннее обновление, запущенное пересборкой, больше не запускает
    пересборку ещё раз.
  • macOS-скрипт установки больше не сносит данные при обновлении.
    install-macos.sh (0.2) переносил только состояния визарда; всё
    остальное уезжало в Корзину со старым бандлом: settings.json
    (идентификатор устройства — подписки с привязкой видели «новое
    устройство», язык, режим движка ядра, debug API), сертификат сопряжения
    с демоном («client certificate not trusted» после обновления),
    remote-машины (вкладка Remote пустела) и кэши подписок — ровно то, что
    взводило цикл из предыдущего пункта. Скрипт (0.3) бэкапит весь каталог
    данных и восстанавливает его, не затирая файлы из нового архива.

Техническое / Внутреннее

  • buildSnapshotFromRawCache возвращает ErrRawCacheIncomplete
    (→ auto-update fallback) только когда кэша нет ни у одной включённой
    подписки; частично отсутствующий кэш деградирует источник с
    предупреждением, не блокируя сборку. Регрессионный тест приложен; URL
    недостающего источника в тесте — 127.0.0.1:1, чтобы тест не зависел
    от внешней сети.
  • У UpdateConfigFromSubscriptions появился внутренний флаг
    triggerRebuild: хвостовая пересборка пропускается, когда обновление
    само запущено fallback'ом RebuildConfigIfDirty — это ребро и было
    рекурсией.
  • Диалог с командой (kickstart демона после обновления ядра, Linux
    capabilities) рисует сообщение и команду обычным цветом и подстраивает
    высоту под содержимое — раньше оба рисовались цветом отключённого
    placeholder'а, почти невидимым на тёмной теме (вошло в тег v1.5.0,
    здесь для полноты).

v1.5.0 — included in this release

The v1.5.0 tag was never published: a configurator crash was caught minutes before release. Everything below shipped for the first time in v1.5.1.

Тег v1.5.0 не публиковался: краш конфигуратора был пойман за минуты до релиза. Всё, что ниже, впервые вышло в составе v1.5.1.

Крупный цикл: маршрут теперь можно провести через несколько хопов подряд
(цепочки), Направления заменили каналы, DNS настраивается формами, подписка
сворачивается в группу одной галкой, а настройки переносятся между десктопом и
телефоном одним файлом. Плюс аудит всего цикла: 21 исправленный дефект, из них
несколько таких, при которых ядро отвергало конфиг целиком и пользователь
оставался без VPN.

EN

Highlights

  • Pinned core: sing-box-lx 1.14.0-lx.27. Chains need it (with_lx_chain); the launcher downloads the pinned version itself when the installed one differs.

  • The traffic profiler window is now translated, and the translation mechanism was rebuilt. The launcher's strings now use the English text itself as the translation key (the same model as LxBox on the phone), with two CI checks guaranteeing that no string can be added without a translation and no stale entry can linger in the catalog. Visible consequences: the traffic window — previously English-only — is now fully translated; core diagnostics (chain validation, backup errors) are now consistently English in every language, since raw technical messages pass through untranslated by design. Translation files from older versions are not readable by this release — on the first launch after the update the launcher notices this and refreshes the catalogs itself in the background (a restart shows them); Settings → Download translations still forces it manually.

  • A route can now go through several hops in a row (chains). Add a hop chain from the ⋮ menu on the Sources tab: pick two or more positions, and traffic walks them one after another — you → hop 1 → hop 2 → the site. Any position may be a node, a subscription group or a Direction, so switching a group changes the path without a restart, and a node used twice is not duplicated in the config. This was previously only expressible node-by-node via detour, which cannot route through a group and cannot be chained past two hops. A chain then behaves like any other server: your Directions pick it up, you can select it in the proxy list, and an auto-select group measures the whole multi-hop route against direct nodes. The order shown is the order packets travel: the top entry is the hop closest to you, the bottom one is the address the site sees. The form refuses the combinations the core rejects — fewer than two positions, a duplicate, a stripped ClientHello fingerprint on a reality node — because the core turns any of those into a config that will not start at all. Chains need a core built with with_lx_chain; on an older core the chain simply does not appear among your nodes, and the launcher says which core you have and what is missing rather than leaving you without a VPN.

  • A chain can be measured layer by layer. The node Info window for a chain lists its positions and probes them cumulatively — you → hop 1, you → hop 1 → hop 2, … — so the number next to each hop is the price of the whole path up to it, and the (+N) delta names the hop that added the latency. The composition is re-read before every probe: when a position is a group, switching the group's pick changes the path without a restart, and "Probe again" measures the new route. Two runs per click, the first warms up tunnels (WG handshake, QUIC session) and is not shown — otherwise the numbers would jump with no explanation. The same probe is exposed to the debug API, measuring through the same endpoint as the UI.

  • The single latency test budget is now a setting (ping dialog on the proxies tab, stored in settings.json). Chains made the old fixed 5 seconds insufficient: a multi-hop route can honestly cost more, and it used to show "timeout" instead of its real price. All three transports (local Clash API, macOS daemon, remote machine) honor the same budget — previously their internal deadlines (15–25 s) silently capped it.

  • Chain fine-tuning from the form. Per-protocol option overrides (rewrite) are edited as rows of "protocol · parameter · JSON value" — null deletes a key (RFC 7396); an invalid value or a duplicate pair is flagged in place, and an empty one is an error rather than a silently dropped row. The chain's name is edited in the same form (with a warning when other chains reference the old name, when the new name is already taken, or when a referenced chain is declared lower in the list than this one). Creating a chain opens its editor immediately — an empty chain is not a thing to leave behind a ⋮ menu.

  • Sources rows show node counts. Each subscription row displays how many nodes it yields and how many are actually enabled (12 / 50 nodes) — counted lazily in the background the first time you open the tab, cached, and recounted after any change to the source list or a subscription refresh.

  • A missing TUN option is now settable: Auto-route system traffic (issue #106). The auto_route flag was hard-coded to true — there was no checkbox for it, and editing config.json by hand did nothing because the file is regenerated from your settings on every build. It is now an ordinary setting on the Settings tab, next to Strict route and TUN stack. (TLS fragmentation, mentioned in the same report, was already configurable — three settings under TLS fragment.)

  • Template conditions can nest, and every engine keyword is now marked. A condition may combine #and/#or to any depth, so a rule like "TUN is on, or we are on Windows with the proxy inbound enabled" no longer needs to be split across several template fields. Alongside this, the language finally follows one rule end to end: # marks an engine keyword, @ marks a variable, everything else is data. Keywords that used to be written bare — and, or, value, else, on_change, set — are now #and, #or, #value, #else, #on_change, #set. Old spellings keep working indefinitely, so existing templates are unaffected.

  • A new #enable gate replaces if / if_or / enabled on template nodes. One marked key decides whether a node exists at all — a preset fragment, a settings row, a params block — and it accepts the full condition language, not just a list of booleans. The old keys remain valid.

  • Settings rows now update one at a time. Toggling a switch used to rebuild the whole Settings tab; now each row knows which variables its condition depends on, and only the affected rows change state. A disabled row also greys out its label, so the link between a switch and the fields it controls is visible rather than merely enforced.

  • DNS groups: several resolvers behind one name. When adding a DNS server you can now pick Group — it queries its members and takes the fastest answer, so one dead resolver no longer stalls name resolution. Rules and the DNS default point at the group by a single tag. The same server type already existed on the phone; the two sides now describe DNS the same way.

  • Rules now point at Directions, and a Direction is something you shape. A Direction is a named routing target with its own node filter, an optional auto-select twin, and a name you pick: Directions tab. This matters because subscription node tags are regenerated on every update — a rule pointing at a node breaks the moment the provider renames it, while a rule pointing at a Direction survives. The filter is now the plain body of a regular expression plus an "invert" tick instead of hand-written /…/i, matching ignores case, and emoji flags work. A Direction whose filter catches nothing now blocks its rules' traffic instead of quietly letting it out past the VPN, and says so. The tab's shared JSON editor is gone: subscriptions are edited on Sources, and raw JSON stayed inside a single Direction's window.

  • DNS servers are now set up with a form, not by writing JSON. Adding a resolver used to mean typing a raw sing-box object into an empty box: the type, the port, the DoH path, the TLS server name, the resolver for its own hostname. There is now a form for each of the five kinds people actually use — UDP, TCP, DoT, DoH and a group — with the fields that kind needs and nothing else. The channel a DNS query travels through is picked from the list of Directions, the same list a rule picks from, instead of being typed in by hand. Raw JSON stays available on its own tab: it is the only way to reach the types without a form (hosts, fakeip, dhcp, quic/h3).

  • The DNS library gained Shield DNS and six more resolvers, and its servers now carry their own settings. Google (UDP/DoT), Cloudflare DoT, OpenDNS, Quad9, a Safe DNS profile (Quad9 anti-malware, AdGuard, AdGuard Family) and Shield DNS — a group that asks nine providers at once and takes the fastest answer. Each of them exposes what is worth changing: which address of the provider to use, which channel to reach it through, which profile to enforce. All arrive switched off — nothing about your current setup changes until you turn one on. Turning on a group turns on its members too, and members that stay off are struck through in the list rather than silently dropped.

  • A subscription can now be folded into a single group — one checkbox instead of four. A fifty-node subscription no longer has to arrive in the directions list as fifty entries: tick Fold this subscription into a group on Sources → Edit, and it comes as one. What it folds into is picked on the new Group tab — a selector you choose from by hand, an auto-select group that measures latency, or a selector with an auto-select group as its default (the same auto-select settings a Direction has). This replaces the four checkboxes Local auto, Local select, Exclude from global and Expose tags, of whose eight combinations exactly one was useful — and expressing it took three ticks at once. Existing settings are read and converted on load; nothing needs re-entering.

  • A subscription's groups are no longer offered as rule targets, and no longer clutter the Directions list. They are a grouping and sugar for a Direction, not something a rule should hold onto: such a target disappears when the subscription is deleted and is renamed when its prefix changes, leaving the rule pointing nowhere. A rule that pointed at a target that no longer exists is switched to direct when settings load, and says so in the log — a disabled rule would look like routing had silently broken. Renaming a subscription's prefix now renames its groups on its own: they are rebuilt from the current prefix on every build instead of being stored and patched.

  • Settings can now be carried between the desktop launcher and LxBox on your phone (LX Backup). Settings → Backup exports subscriptions, servers, rules, DNS and portable variables into one file, and imports one back. Anything the other side has no place for — mobile-only matchers, desktop-only source filters — travels along untouched and comes back intact, so a backup that passed through the phone does not return impoverished. Directions travel with the rules that point at them, so a rule whose target came in the same file now arrives working — until now a rule pointing at the phone's vpn-3 landed here switched off. A rule pointing at a target that exists in neither place is still imported switched off rather than lost or silently enabled: an enabled rule with a dead target makes the core reject the whole config. Settings that mean something different on another machine (paths, interfaces) are not applied, and you are told which ones. Passwords and keys are stored as plain text — the export says so before you send the file anywhere.

  • Subscriptions that serve a WireGuard .conf or an Amnezia vpn:// profile now work. Previously such a body fell through to line-by-line URI parsing, found no links, and produced zero nodes without a single message. A .conf may carry several [Interface] sections — each becomes a node; a vpn:// profile with several locations now yields all of them instead of one, and uncompressed profiles (which Amnezia also exports) are accepted.

  • Eight ways a shared link could lose data are fixed. Copying a link out of the launcher and importing it back now yields the same node — checked across the whole shared corpus. Among the losses: a node without TLS was exported with sni=, so the link described a TLS node that would not connect; post-quantum VLESS lost its key exchange; AnyTLS lost its entire REALITY block; hysteria2/gecko lost its packet-size bounds; VMess lost XHTTP mode and path; an HTTP proxy with a password but no username had the password re-imported as the username.

  • A Direction saved without changes no longer wipes its preset filter. Opening proxy-out and pressing Save used to leave behind an empty override that masked the russian preset's exclude 🇷🇺 nodes filter — so Russian traffic could leave through a Russian server, exactly what the preset guards against. Such empty overrides are no longer written and are cleaned up on load.

  • A broken reference can no longer take the whole config down. The core rejects the entire config.json over one dangling reference or dependency cycle — and until now several ways to produce one slipped through: a node sitting inside the group it dials through transitively, a chain whose hop vanished after a subscription update, a group emptied by a cascade of removals, a default pointing outside its group, DNS rules and dns.final referencing a pruned server. The build now runs one final pass over the whole dependency graph (group members, detours, chain positions, DNS references alike): whatever would not start degrades that one element with a log warning, and the config stays valid. The same pass enforces the chain invariants the core only checks at startup — where sing-box check stays silent and the failure used to surface as a VPN that would not come up.

  • Clearing a field now sticks. An override cleared by the user (an emptied field in a Direction form) used to be indistinguishable from a stale artifact and was dropped on load — the template value silently came back. The launcher now records that the clearing was deliberate and keeps it.

  • LX Backup no longer loses data on round-trip. Chains survive export → import (they used to vanish entirely); the DNS section is now applied on import (it was exported and then ignored); a subscription's local outbound groups are restored (written but never read back); WARP registrations travel too, and importing does not overwrite a live registration on the receiving machine. Fields and per-entry extension blobs the launcher does not understand are preserved and returned intact on the next export instead of being dropped.

  • Fixed nodes lost or corrupted between apps. Copying a link out of the launcher kept only part of the XHTTP tuning (xmux, upload limits) — the imported node looked identical but behaved differently; hysteria2 certificate pins (pinSHA256) never reached the config at all, so self-signed servers could not connect; a junk fp= overrode a valid fingerprint= from the same link; a + in a WebSocket path or a WireGuard key was decoded into a space, yielding 404s and "not base64" rejections; AmneziaWG nodes with overlapping magic headers took the whole config down instead of being skipped.

  • DNS configurator honesty fixes. Rebuilding the server list no longer re-enables group members you switched off; editing a server through the form no longer strips fields the form does not show (a disabled server stayed disabled, description and TLS extras survive); JSON-tab edits are no longer overwritten by the form snapshot when you flip between tabs; Preview and remote deploy no longer leak @dns_* placeholders into the generated config.

  • Form edits that used to vanish now save. The chain form's idle_timeout, a renamed chain with no positions yet, the fold ("Group") tab's interval/URL/mode — each had a path where Save silently kept the old value. All collected on Save now.

  • Rules are reordered by drag-and-drop instead of ↑/↓ buttons.

  • A single latency test now has one shared budget across all transports (5 seconds by default, configurable — see above), so a dead node answers "timeout" in the same time regardless of protocol.

  • Blocking a rule's traffic is selectable again. The rule target dropdown listed only subscription and preset outbounds plus the reject/drop actions — the template's own block-out never appeared, so there was no way to pick "block" even though the outbound was in the generated config and worked. Outbounds declared by the template are now offered too.

  • Xray-JSON subscriptions no longer drop XHTTP tuning settings (SPEC 102). When a subscription ships a ready-made Xray/V2Ray config (a JSON array of profiles) instead of share-URI links, nodes using the xhttp/splithttp transport now carry their full extra block (uplink HTTP method, padding size, sc* stream-upload limits) into the generated config, matching what the share-URI path already supported. Previously only path/host/mode survived, so servers with stricter XHTTP settings rejected the request with 400 Bad Request and the node didn't work. The nested xmux connection-pool block is now supported too.

  • More subscription nodes now work instead of being silently dropped (SPEC 103). WireGuard links that carry the private key as a query parameter, or omit allowed_ips entirely, are no longer discarded — both forms are common in the wild and other clients accept them. HTTP(S) CONNECT proxies (proxy-http://, proxy-https://) are now supported as subscription entries. Hysteria2 nodes using the gecko obfuscation now keep it (the core supports it; it used to be stripped), and a node whose obfuscation has no password keeps working with obfuscation off instead of taking the whole config down with it.

  • Fixed nodes that connected but carried no traffic, or failed with 404. WebSocket paths that arrive double-encoded (a common panel quirk) are now decoded before use, so the request reaches the path the server actually serves. The same applies to httpupgrade, which previously left an Xray early-data tail inside the path. VLESS nodes using the post-quantum encryption layer now keep it on desktop as well.

  • A broken uTLS fingerprint no longer changes how a node behaves between restarts. An unrecognized value is replaced with chrome and flagged on the node, rather than being dropped on one code path, randomized on another, and removed entirely on a third.

  • IPv6 in the tunnel is now an explicit switch. Settings has an Enable IPv6 checkbox (off by default). Turning it on adds an IPv6 address to the TUN interface and lets name resolution return IPv6; leaving it off resolves IPv4 only. Previously IPv6 was suppressed implicitly in several places at once, so sites that only answer over IPv6 failed to open with no visible reason.

  • The UI translation layer now keys off the English text itself. A string in the code is its translation key, so English no longer lives in a separate catalogue that had to be edited in lockstep with every code change — 62% of recent commits touched it, and 130 of its keys had already gone dead. Russian keeps its catalogue (bin/locale/ru.json); a checker fails the build when code and catalogue disagree, and a stale catalogue heals itself after an update instead of leaving the UI half-translated.

Technical / Internal

  • The dialog that shows a terminal command (daemon-service restart after a core update, Linux capabilities) now renders its text in the normal foreground color and sizes itself to the message. Previously both the explanation and the sudo command were drawn in the disabled/placeholder color — nearly invisible on the dark theme — inside a box ten rows tall regardless of content.
  • The condition engine is now one implementation with three drivers. #if (substitution), #enable (existence gate) and carrier guards all evaluate through the same code path in both apps, so a condition means the same thing wherever it is written. Dependencies of a condition are extracted statically at template load — that index is what makes per-row UI updates possible, and it is itself part of the shared contract (contract/corpus/template/deps/, 16 fixtures both apps run). Six real regressions surfaced while unifying the paths, each covered by a test: bare variable names in legacy guards, the platform scope of an operand, a JSON predicate written as a string, if+if_or on one carrier, list splicing in the mobile engine, and enabled being stripped from nested sing-box objects.
  • The template engine now behaves identically on desktop and mobile, checked by a shared corpus of 70 fixtures (contract/corpus/template/) that both apps run. Three real defects surfaced and were fixed on the desktop side: a numeric template variable was only converted to a JSON number when its name appeared in a hard-coded list of four, so any newly added numeric setting reached the core as a string and the core rejected the whole config; an invalid #if condition (missing both and and or) evaluated as true, pulling a block into the config that the template author never asked for; and an unresolved variable either silently became an empty string or aborted the build, instead of leaving the misspelled name visible in the output. On the mobile side, boolean values now compare with surrounding whitespace and mixed case handled, #notEmpty reads the variable's declared type rather than the length of its printed form, list-typed variables exist at all, and a condition's right-hand side accepts another variable.
  • Template conditions can now be named, and more than one can sit on the same object: alongside "#if" the engine accepts any suffixed form — "#if1", "#if 2", "#if tun-only". JSON allows only one key of a given name, so a second plain "#if" on the same object used to overwrite the first one silently; a suffix makes both apply and doubles as a readable label. Multiple conditions on one object are applied in sorted key order, identically in the launcher and in LxBox. Plain "#if" keeps working unchanged.
  • XHTTP transport assembly (node_parser_transport.go) is now one shared builder used by both the share-URI parser and the Xray-JSON converter, so the two paths can no longer drift apart on which fields they understand — a parity test guards this. A malformed or non-object extra degrades the node (fields it would have set are simply omitted) instead of breaking the config.
  • The parser is now checked against a shared conformance corpus (contract/corpus/, 282 fixtures) that the LxBox mobile app runs too, so the same subscription yields the same nodes on both platforms. Emitted nodes no longer carry values the core defaults on its own (WireGuard mtu/name/system), which also makes a node's identity hash stable across the two projects. short_id values the core would reject (odd hex length, over 16 chars) are dropped rather than truncated into a different value, and utls is no longer emitted on QUIC protocols, where it has no meaning.

RU

Основное

  • Пин ядра: sing-box-lx 1.14.0-lx.27. Цепочкам оно и нужно (with_lx_chain); лаунчер сам скачивает закреплённую версию, если установленная отличается.

  • Окно профайлера трафика переведено, а механика переводов перестроена. Строки лаунчера теперь используют сам английский текст как ключ перевода (та же модель, что в LxBox на телефоне); два CI-чекера гарантируют, что строку нельзя добавить без перевода, а мёртвая запись не залежится в каталоге. Заметные следствия: окно трафика — раньше только английское — переведено целиком; диагностика ядра (валидация цепочек, ошибки бэкапа) теперь единообразно английская на любом языке — сырые технические сообщения проходят без перевода намеренно. Файлы переводов от старых версий этот релиз не читает — при первом запуске после обновления лаунчер сам замечает это и в фоне обновляет каталоги (после перезапуска они подхватываются); «Настройки → Скачать переводы» по-прежнему делает это вручную.

  • Маршрут теперь можно провести через несколько хопов подряд (цепочки). Цепочка добавляется из меню ⋮ на вкладке Sources: выбираете две позиции или больше, и трафик идёт через них по очереди — вы → хоп 1 → хоп 2 → сайт. Позицией может быть узел, группа подписки или Направление, поэтому переключение внутри группы меняет путь без перезапуска, а узел, использованный дважды, не дублируется в конфиге. Раньше это выражалось только по одному узлу через detour, который не умеет ходить через группу и не собирается длиннее двух хопов. Дальше цепочка ведёт себя как обычный сервер: её подхватывают ваши Направления, её можно выбрать в списке прокси, а автовыбор сравнит весь многохоповый маршрут с прямыми узлами по задержке. Порядок в списке — это порядок пакета: сверху ближайший к вам хоп, снизу тот, чей адрес видит сайт. Форма не даёт собрать то, что ядро отвергает — меньше двух позиций, повтор, снятый отпечаток ClientHello на reality-узле, — потому что любое из этих условий превращает конфиг в незапускающийся целиком. Цепочкам нужно ядро, собранное с with_lx_chain: на старом ядре цепочка просто не появится среди ваших узлов, а лаунчер сообщит, какое ядро у вас стоит и чего в нём не хватает, вместо того чтобы оставить вас без VPN.

  • Цепочку можно замерить послойно. Окно Info узла-цепочки показывает её позиции и меряет их накопительно — вы → хоп 1, вы → хоп 1 → хоп 2, … — цифра у каждого хопа означает цену всего пути до него, а дельта (+N) называет виновника задержки. Состав перечитывается перед каждым замером: если позиция — группа, переключение её выбора меняет путь без перезапуска, и «Замерить снова» меряет новый маршрут. На нажатие делается два прогона: первый прогревает туннели (WG-хендшейк, QUIC-сессия) и не показывается — иначе цифры скакали бы без объяснения. Та же проба доступна из debug API и меряет тем же эндпоинтом, что и UI.

  • Бюджет одиночного теста задержки стал настройкой (диалог пинга на вкладке прокси, хранится в settings.json). Прежние жёсткие 5 секунд перестали хватать из-за цепочек: многохоповый маршрут честно стоит дороже и показывал «таймаут» вместо своей цены. Все три транспорта (локальный Clash API, macOS-демон, удалённая машина) уважают один бюджет — раньше их внутренние дедлайны (15–25 с) молча его обрезали.

  • Тонкая настройка цепочки из формы. Переопределения опций по протоколам (rewrite) правятся строками «протокол · параметр · JSON-значение» — null удаляет ключ (RFC 7396); невалидное значение и дубль пары подсвечиваются на месте, а пустое — ошибка, а не молча выброшенная строка. Имя цепочки правится в той же форме (с предупреждением, если на старое имя ссылаются другие цепочки, новое имя уже занято или цепочка-позиция объявлена в списке ниже этой). Создание цепочки сразу открывает её редактор — пустой цепочке нечего делать за меню ⋮.

  • На строках Sources — счётчики узлов. У каждой подписки видно, сколько узлов она даёт и сколько из них реально включено («12 / 50 nodes») — считается лениво в фоне при первом открытии вкладки, кэшируется и пересчитывается после любой правки списка источников или обновления подписки.

  • Направление, сохранённое без изменений, больше не стирает пресетный фильтр. Открыть proxy-out и нажать Save оставляло пустой override, который прятал фильтр пресета russian «исключить узлы 🇷🇺» — и российский трафик мог выйти через российский сервер, ровно от чего пресет защищает. Такие пустые override'ы больше не пишутся и вычищаются при загрузке.

  • Битая ссылка больше не валит весь конфиг. Ядро отвергает config.json целиком за одну висячую ссылку или кольцо зависимостей — и до сих пор несколько способов их получить проскакивали: узел внутри группы, через которую он же ходит транзитом; цепочка, чей хоп исчез после обновления подписки; группа, опустевшая каскадом удалений; default вне состава; DNS-правило и dns.final на вычищенный сервер. Теперь сборка делает финальный проход по всему графу зависимостей (участники групп, detour, позиции цепочек, DNS-ссылки — рёбра одного графа): всё, что не запустилось бы, деградирует одним элементом с warning в логе, а конфиг остаётся валидным. Тот же проход держит инварианты цепочек, которые ядро проверяет только на старте — там sing-box check молчит, и отказ раньше выглядел как VPN, который просто не поднялся.

  • Очистка поля теперь сохраняется. Стёртое пользователем значение (опустошённое поле в форме Направления) было неотличимо от старого артефакта и выбрасывалось при загрузке — шаблонное значение молча возвращалось. Теперь лаунчер запоминает, что очистка была сознательной, и хранит её.

  • LX Backup больше не теряет данные на round-trip. Цепочки переживают экспорт → импорт (раньше исчезали полностью); секция DNS применяется на импорте (экспортировалась и игнорировалась); локальные группы подписки восстанавливаются (писались, но не читались); WARP-регистрации тоже переезжают, и импорт не перетирает живую регистрацию принимающей машины. Поля и блобы расширений, которые лаунчер не понимает, сохраняются и возвращаются нетронутыми при следующем экспорте, а не выбрасываются.

  • Починены узлы, терявшиеся или искажавшиеся между приложениями. Скопированная из лаунчера ссылка несла только часть XHTTP-тюнинга (xmux, лимиты аплоада) — импортированный узел выглядел тем же, а вёл себя иначе; пин сертификата hysteria2 (pinSHA256) вообще не доезжал до конфига, и серверы с самоподписанными сертификатами не подключались; мусорный fp= перебивал валидный fingerprint= из той же ссылки; + в WebSocket-пути или ключе WireGuard декодировался в пробел — 404 и «not base64»; узлы AmneziaWG с пересекающимися magic-заголовками роняли весь конфиг вместо того, чтобы быть пропущенными.

  • Честность DNS-конфигуратора. Пересборка списка серверов больше не включает обратно выключенных вами участников группы; правка сервера формой не стирает поля, которых форма не показывает (выключенный сервер остаётся выключенным, description и TLS-настройки выживают); правки на вкладке JSON не перетираются снимком формы при переключении вкладок; превью и remote-деплой не уносят плейсхолдеры @dns_* в собранный конфиг.

  • Правки форм, которые молча пропадали, теперь сохраняются. idle_timeout в форме цепочки, переименование цепочки без позиций, интервал/URL/режим на вкладке «Группа» подписки — у каждого был путь, на котором Save молча оставлял старое значение. Все собираются на Save.

  • Правила переставляются перетаскиванием вместо кнопок ↑/↓.

  • У одиночного теста задержки теперь один общий бюджет для всех транспортов (по умолчанию 5 секунд, настраивается — см. выше): мёртвый узел отвечает «таймаут» за одно и то же время независимо от протокола.

  • Блокировку в правиле снова можно выбрать. В выпадающем списке целей были только outbound'ы подписок и пресетов плюс действия reject/drop — собственный block-out шаблона в него не попадал, и выбрать «блокировать» было нечем, хотя сам outbound уезжал в конфиг и работал. Теперь предлагаются и цели, объявленные шаблоном.

  • Появилась настройка, которой не хватало: «Auto-route system traffic» (issue #106). Флаг auto_route был зашит в true — галки для него не было, а ручная правка config.json ничего не давала, потому что файл пересобирается из ваших настроек при каждом запуске. Теперь это обычная настройка на вкладке Settings, рядом со «Strict route» и «TUN stack». (Фрагментация TLS, упомянутая в том же обращении, настраивалась и раньше — три параметра в блоке «TLS fragment».)

  • Условия в шаблоне научились вкладываться, а все ключевые слова движка получили пометку. Условие может комбинировать #and/#or на любую глубину — правило вида «включён TUN, либо мы на Windows и включён proxy-инбаунд» больше не приходится разносить по нескольким полям шаблона. Заодно язык стал последовательным: # — ключевое слово движка, @ — переменная, остальное — данные. Ключевые слова, которые раньше писались без пометки (and, or, value, else, on_change, set), теперь #and, #or, #value, #else, #on_change, #set. Старое написание поддерживается бессрочно — существующие шаблоны не ломаются.

  • Новый гейт #enable вместо if / if_or / enabled на узлах шаблона. Один помеченный ключ решает, существует ли узел вообще — фрагмент пресета, строка настроек, блок params — и принимает весь язык условий, а не только список булевых переменных. Прежние ключи остаются валидными.

  • DNS-серверы настраиваются формой, а не написанием JSON. Раньше добавить резолвер значило вписать в пустое поле сырой объект sing-box: тип, порт, путь DoH, имя сервера TLS, резолвер для его собственного имени. Теперь на каждый из пяти востребованных видов — UDP, TCP, DoT, DoH и группу — есть форма с теми полями, которые этому виду нужны, и без лишних. Канал, которым идёт DNS-запрос, выбирается из списка Направлений — того же, из которого выбирает правило, — а не вписывается руками. Сырой JSON остался на своей вкладке: только через него доступны типы без формы (hosts, fakeip, dhcp, quic/h3).

  • В библиотеке DNS появились Shield DNS и ещё шесть резолверов, а у серверов — собственные настройки. Google (UDP/DoT), Cloudflare DoT, OpenDNS, Quad9, профиль Safe DNS (Quad9 против вредоносного, AdGuard, AdGuard Family) и Shield DNS — группа, которая спрашивает девять провайдеров разом и берёт самый быстрый ответ. У каждого настраивается то, что имеет смысл менять: какой адрес провайдера использовать, каким каналом до него идти, какой профиль включить. Все приезжают выключенными — пока вы сами не включите, в вашей настройке ничего не меняется. Включение группы включает и её участников, а те, что остались выключенными, показываются в списке зачёркнутыми, а не пропадают молча.

  • Подписку можно свернуть в группу — одна галка вместо четырёх. Подписке на полсотни узлов больше не обязательно приезжать в список Направлений полусотней строк: галка «Свернуть подписку в группу» на Источники → Правка — и она приезжает одной. Во что именно сворачивать, выбирается на новой вкладке Группа: селектор с ручным выбором, автогруппа по задержке или селектор с автогруппой в качестве умолчания (настройки автовыбора те же, что у Направления). Это заменяет четыре галки Local auto, Local select, Exclude from global и Expose tags, из восьми комбинаций которых полезной была ровно одна — и выражалась она тремя галками разом. Прежние настройки читаются и переводятся в новую форму при загрузке, перенастраивать ничего не нужно.

  • Группы подписок больше не предлагаются целями правил и не засоряют список Направлений. Это группировка и сахар к Направлению, а не то, за что правилу стоит держаться: такая цель исчезает вместе с подпиской и переименовывается вместе с её префиксом, и правило начинает указывать в никуда. Правило, целившееся в исчезнувшую цель, при загрузке переключается на direct и сообщает об этом в лог — выключенное правило выглядело бы как молча сломавшаяся маршрутизация. Смена префикса подписки теперь переименовывает её группы сама: они собираются из текущего префикса на каждой сборке, а не хранятся и правятся на месте.

  • Группы DNS: несколько резолверов под одним именем. При добавлении DNS-сервера появился выбор Группа — она опрашивает своих участников и берёт самый быстрый ответ, так что одна мёртвая нода больше не вешает разрешение имён. Правила и DNS по умолчанию ссылаются на группу одним тегом. На телефоне такой тип сервера уже был; теперь обе стороны описывают DNS одинаково.

  • Правила теперь ссылаются на Направления, и Направление — это то, что вы настраиваете. Направление — именованная цель со своим отбором узлов, при желании автовыбором и именем на ваш вкус: вкладка Направления. Это важно потому, что теги узлов подписки создаются заново при каждом обновлении: правило, указывающее на узел, ломается, как только провайдер его переименует, а правило на Направление — нет. Отбор теперь задаётся телом регулярного выражения и галкой «инверсия» вместо /…/i руками, регистр не учитывается, флаги-эмодзи работают. Направление, чей отбор никого не поймал, теперь блокирует трафик своих правил, а не выпускает его молча мимо VPN, — и сообщает об этом. Общий редактор JSON с вкладки убран: подписки правятся на Sources, а сырой JSON остался внутри окна одного Направления.

  • Настройки теперь переносятся между лаунчером и LxBox на телефоне (LX Backup). Settings → Резервная копия выгружает в один файл подписки, серверы, правила, DNS и переносимые переменные — и загружает обратно. То, чему на другой стороне нет места (мобильные матчеры по приложениям, десктопные фильтры источников), едет вместе с файлом нетронутым и возвращается целым: копия, побывавшая на телефоне, не должна вернуться обеднённой. Направления едут вместе со ссылающимися на них правилами, поэтому правило, чья цель приехала в том же файле, приходит рабочим — до сих пор правило на телефонный vpn-3 попадало сюда выключенным. Правило, чьей цели нет ни там, ни здесь, по-прежнему импортируется выключенным, а не теряется и не включается: включённое правило с мёртвой целью заставляет ядро отвергнуть весь конфиг. Настройки, которые на другой машине означают другое (пути, интерфейсы), не применяются — и вам говорят, какие именно. Пароли и ключи лежат в файле открытым текстом, и экспорт предупреждает об этом до того, как файл куда-то уедет.

  • Заработали подписки, отдающие WireGuard .conf или профиль Amnezia vpn://. Раньше такое тело уходило в построчный разбор ссылок, не находило ни одной и давало ноль узлов — молча, без единого сообщения. В .conf может быть несколько секций [Interface] — каждая становится узлом; профиль vpn:// с несколькими локациями теперь даёт все, а не одну, и принимаются несжатые профили, которые Amnezia тоже экспортирует.

  • Закрыты восемь способов потерять данные при копировании ссылки. Ссылка, скопированная из лаунчера и импортированная обратно, теперь даёт тот же узел — проверено на всём общем корпусе. Из потерянного: узел без TLS выгружался с sni=, и ссылка описывала TLS-узел, который не подключается; post-quantum VLESS терял обмен ключами; AnyTLS терял весь блок REALITY; hysteria2/gecko — границы размера пакета; VMess — режим и путь XHTTP; у HTTP-прокси с паролем без логина пароль при обратном разборе становился логином.

  • Строки настроек обновляются по одной. Раньше переключение любой галки пересобирало вкладку Settings целиком; теперь каждая строка знает, от каких переменных зависит её условие, и состояние меняют только затронутые. У неактивной строки вдобавок приглушается подпись — связь между переключателем и полями, которыми он управляет, стала видимой, а не только действующей.

  • Подписки в формате Xray-JSON больше не теряют настройки XHTTP (SPEC 102). Если подписка отдаёт готовый Xray/V2Ray-конфиг (JSON-массив профилей) вместо ссылок share-URI, узлы с транспортом xhttp/splithttp теперь переносят в конфиг весь блок extra (метод uplink-запроса, размер padding, лимиты sc* для потоковой загрузки) — так же, как это уже умела ветка share-URI. Раньше сохранялись только path/host/mode, и сервер с более строгими требованиями к XHTTP отклонял запрос ошибкой 400 Bad Request — узел не работал. Дополнительно теперь поддержан вложенный блок xmux (пул соединений).

  • Из подписок теперь доезжает больше рабочих узлов (SPEC 103). WireGuard-ссылки, где приватный ключ лежит в query-параметре или вовсе не указан allowed_ips, больше не выбрасываются — обе формы часто встречаются у провайдеров, и другие клиенты их принимают. Поддержаны HTTP(S) CONNECT-прокси (proxy-http://, proxy-https://) как записи подписки. Узлы hysteria2 с обфускацией gecko сохраняют её (ядро её умеет, раньше она срезалась), а узел с обфускацией без пароля продолжает работать без неё, вместо того чтобы уронить весь конфиг.

  • Починены узлы, которые подключались, но не пропускали трафик, или отдавали 404. WebSocket-путь, пришедший дважды закодированным (частая особенность панелей), теперь раскодируется перед использованием — запрос уходит на тот путь, который сервер действительно отдаёт. То же для httpupgrade: раньше в пути оставался хвост early data от Xray. Узлы VLESS с пост-квантовым слоем шифрования теперь сохраняют его и на десктопе.

  • Битый uTLS-отпечаток больше не меняет поведение узла между запусками. Нераспознанное значение заменяется на chrome с пометкой на узле — вместо того чтобы на одном пути отбрасываться, на другом становиться случайным, а на третьем сниматься целиком.

  • IPv6 в туннеле стал явной настройкой. На вкладке Settings появилась галка «Enable IPv6» (по умолчанию выключена). При включении в TUN-интерфейс добавляется IPv6-адрес и разрешается IPv6 при разрешении имён; при выключении имена резолвятся только в IPv4. Раньше IPv6 глушился неявно сразу в нескольких местах, и сайты, доступные только по IPv6, не открывались без видимой причины.

  • Слой перевода интерфейса теперь опирается на сам английский текст. Строка в коде и есть её ключ перевода: английский больше не живёт отдельным каталогом, который приходилось править синхронно с каждой правкой кода — его трогали 62% недавних коммитов, а 130 его ключей уже были мёртвыми. У русского каталог остался (bin/locale/ru.json); чекер валит сборку при расхождении кода и каталога, а устаревший каталог самолечится после обновления, вместо того чтобы оставить интерфейс наполовину переведённым.

Техническое / Внутреннее

  • Диалог с командой для терминала (перезапуск службы демона после обновления ядра, Linux capabilities) теперь рисует текст обычным цветом и подстраивает высоту под сообщение. Раньше и объяснение, и sudo-команда рисовались цветом отключённого поля — почти невидимым на тёмной теме — в блоке высотой в десять строк независимо от содержимого.
  • Движок условий стал одной реализацией с тремя драйверами. #if (подстановка), #enable (гейт существования) и гейты носителей вычисляются одним и тем же кодом в обоих приложениях — условие значит одно и то же, где бы оно ни было написано. Зависимости условия извлекаются статически при загрузке шаблона; именно этот индекс делает возможным обновление UI по одной строке, и он сам часть общего контракта (contract/corpus/template/deps/, 16 фикстур гоняют оба приложения). При объединении путей вскрылись шесть настоящих регрессий, каждая закрыта тестом: голые имена переменных в легаси-гейтах, учёт платформы операнда, предикат-JSON, записанный строкой, if+if_or на одном носителе, сплайсинг списка в мобильном движке и удаление enabled у вложенных объектов sing-box.
  • Движок шаблонов теперь ведёт себя одинаково на десктопе и на мобиле — это проверяет общий корпус из 70 фикстур (contract/corpus/template/), который гоняют оба приложения. Прогон вскрыл три настоящих дефекта на стороне десктопа, все исправлены: числовая переменная шаблона превращалась в JSON-число, только если её имя было в захардкоженном списке из четырёх — то есть любая новая числовая настройка уезжала в ядро строкой, и ядро отвергало весь конфиг; невалидное условие #if (без and и без or) считалось истинным и втаскивало в конфиг блок, которого автор шаблона не просил; неразрешённая переменная либо молча становилась пустой строкой, либо роняла всю сборку — вместо того чтобы оставить опечатку видимой в готовом конфиге. На стороне мобилы: булевы значения теперь сравниваются с учётом пробелов и регистра, #notEmpty смотрит на объявленный тип переменной, а не на длину её строкового вида, появился списочный тип, и правая часть условия принимает не только литерал, но и другую переменную.
  • Условиям в шаблоне можно давать имена, и на одном объекте их теперь может быть несколько: кроме "#if" движок принимает любую форму с суффиксом — "#if1", "#if 2", "#if tun-only". JSON не допускает двух одинаковых ключей, поэтому второй обычный "#if" на том же объекте раньше молча затирал первый; суффикс снимает это ограничение и заодно служит понятной подписью условия. Несколько условий одного объекта применяются в порядке сортировки ключей — одинаково в лаунчере и в LxBox. Обычный "#if" работает как раньше.
  • Сборка XHTTP-транспорта (node_parser_transport.go) вынесена в общее ядро, которым пользуются и парсер share-URI, и конвертер Xray-JSON — набор понимаемых полей у двух веток больше не может разойтись, это проверяет отдельный тест на паритет. Битый или не-объектный extra деградирует узел (поля, которые он бы задал, просто не пишутся), а не ломает конфиг.
  • Парсер проверяется общим конформанс-корпусом (contract/corpus/, 282 фикстуры), который гоняет и мобильное приложение LxBox, — одна и та же подписка даёт одинаковые узлы на обеих платформах. В узлы больше не пишутся значения, которые ядро подставляет само (mtu/name/system у WireGuard); заодно это делает identity-хеш узла одинаковым в обоих проектах. Значения short_id, которые ядро отвергает (нечётная длина, длиннее 16 символов), отбрасываются, а не обрезаются в другое значение; utls больше не пишется у QUIC-протоколов, где он не имеет смысла.

Don't miss a new singbox-launcher release

NewReleases is sending notifications on new releases.