Skip to content

release v1.2.4

Choose a tag to compare

@github-actions github-actions released this 15 Jul 21:12

Release v1.2.4

Downloads

macOS (Universal) - Supports both Apple Silicon and Intel

Option 1: Installation Script (Recommended)

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

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

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.2.4-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.2.4-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.2.4-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.2.4 — null в urltest-опциях больше не ломает конфиг

Core pinned: sing-box-lx 1.14.0-lx.5 (unchanged from v1.2.3). No config migration needed.

A one-bug release: a launcher-generated config.json could carry "interval": null on a urltest outbound, and sing-box refused to start. If you hit it, this version repairs your state on the next launch by itself.

EN

Highlights

  • "interval": null in a urltest outbound no longer breaks the config — and a state file already poisoned with it heals itself. The launcher could emit "interval": null, "tolerance": null, "url": null on an auto/urltest outbound, after which the core refused the config outright: decode config at config.json: outbounds[264].interval: time: invalid duration "". The cause was a broken contract between two halves of the outbound-patch machinery: the diff step encoded "the user removed this key" as nil, but the merge step never honoured that meaning and copied the nil in as a live value, which the emitter then marshalled as literal null. Two things made it nasty. First, the nil was written into state.json as a real JSON null and restored faithfully on load, so the breakage survived restarts and config rebuilds — "the new version builds a fresh config at startup" did not help, because the fresh config was rebuilt from the already-poisoned state. Second, the trigger did not require any user edit: the SPEC 058 migration runs OutboundFieldDiff over legacy state on its own, so a urltest whose stored body lacked these keys got all three tombstoned automatically. A null override is now read as "no override — take the value from the base", so the affected outbound gets its template values back (interval: 5m, tolerance: 100, url: https://cp.cloudflare.com/generate_204) on the very next read. No migration and no manual editing: launch the new version and the node is back. (issue #91)

Technical / Internal

  • core/build/preset_outbounds.goapplyOutboundUpdate now skips nil values in patch.Options instead of merging them, so the key falls through to target.Options. This is the read-side heal: the nulls already sitting in users' state.json stop reaching the emitter, without a migration pass rewriting anyone's file.
  • core/build/outbound_diff.gomapDiff no longer writes tombstones for keys present in base but absent from form. With merge ignoring them, continuing to emit them would only accumulate dead weight in state.json; newly written patches are clean. Removing a key is expressed through the outbound type, not the patch: the selector branch in edit_dialog.go already drops url/interval/tolerance on a urltest→selector switch and never depended on the tombstone.
  • Tests: mapDiff/OutboundFieldDiff had no coverage at all — which is exactly how a contract described in a docstring but never implemented shipped in v0.9.7 (eb3067b, SPEC 058) and survived every release since. Added core/build/outbound_diff_test.go (changed/new keys, equal maps, removed key not tombstoned, urltest→selector produces no null options) plus two nil-merge regressions in preset_outbounds_test.go.
  • core/config/varsubst.go is not implicated, despite looking related. It resolves @urltest_interval-style placeholders — a different, older bug (v0.8.8.1) — and it skips nil values entirely, since it only touches values that type-assert to string.

Notes

  • Affects subscription/auto outbounds, not the local wizard's. defaultLocalURLTestOptions always carries concrete values and is cloned per use, so the local-auto path could never produce nulls. That matches the report, where the failure landed on a large subscription (outbounds[264]).
  • Versions affected: v0.9.7 through v1.2.3. The tombstone arrived in v0.9.7. v0.9.5 has no such code of its own, but its state gets rewritten on upgrade — which is why the issue reproduced on both.

RU

Основное

  • "interval": null в urltest-outbound больше не ломает конфиг — а уже испорченный state чинится сам. Лаунчер мог записать "interval": null, "tolerance": null, "url": null в auto/urltest-outbound, после чего ядро отказывалось принимать конфиг целиком: decode config at config.json: outbounds[264].interval: time: invalid duration "". Причина — разъехавшийся контракт между двумя половинами механизма патчей: шаг diff кодировал «юзер удалил этот ключ» как nil, но шаг merge этот смысл никогда не реализовывал и копировал nil внутрь как живое значение, а эмиттер печатал его буквальным null. Неприятным это делали две вещи. Во-первых, nil попадал в state.json настоящим JSON-null и при загрузке честно восстанавливался обратно, поэтому поломка переживала перезапуски и пересборку конфига — «новая версия при старте собирает новый конфиг» не спасало, потому что новый конфиг собирался из уже испорченного состояния. Во-вторых, для срабатывания не требовалось никакой правки руками: миграция SPEC 058 сама гоняет OutboundFieldDiff по legacy-состоянию, так что urltest, в сохранённом теле которого этих ключей не было, получал все три надгробия автоматически. Теперь null-оверрайд читается как «оверрайда нет — берём значение из базы», и пострадавший outbound получает обратно свои шаблонные значения (interval: 5m, tolerance: 100, url: https://cp.cloudflare.com/generate_204) на первом же чтении. Ни миграции, ни правки руками: запустите новую версию — нода вернётся. (issue #91)

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

  • core/build/preset_outbounds.goapplyOutboundUpdate теперь пропускает nil-значения в patch.Options, а не мержит их, и ключ проваливается в target.Options. Это и есть исцеление на чтении: null, уже лежащие в state.json у пользователей, перестают доезжать до эмиттера, и при этом ничей файл не переписывает миграция.
  • core/build/outbound_diff.gomapDiff больше не пишет надгробия для ключей, которые есть в base, но отсутствуют в form. Раз merge их игнорирует, продолжать их генерировать значило бы копить в state.json мёртвый груз; новые патчи пишутся чистыми. Удаление ключа выражается типом outbound'а, а не патчем: selector-ветка в edit_dialog.go и так удаляет url/interval/tolerance при переключении urltest→selector и на надгробие никогда не опиралась.
  • Тесты: у mapDiff/OutboundFieldDiff не было покрытия вообще — именно так контракт, описанный в docstring, но не реализованный, уехал в релиз в v0.9.7 (eb3067b, SPEC 058) и пережил все последующие. Добавлен core/build/outbound_diff_test.go (изменённые/новые ключи, равные map'ы, удалённый ключ без надгробия, urltest→selector без null-опций) плюс две регрессии на nil-merge в preset_outbounds_test.go.
  • core/config/varsubst.go к этому не причастен, хотя выглядит соседом по теме. Он резолвит плейсхолдеры вида @urltest_interval — другой, более старый баг (v0.8.8.1) — и nil-значения пропускает полностью, поскольку трогает только значения, которые type-assert'ятся в string.

Примечания

  • Касается outbound'ов подписок/auto, а не локального визарда. defaultLocalURLTestOptions всегда несёт конкретные значения и клонируется на каждое использование, поэтому local-auto путь не мог породить null. Это сходится с репортом, где падение пришлось на большую подписку (outbounds[264]).
  • Затронутые версии: с v0.9.7 по v1.2.3. Надгробие появилось в v0.9.7. В самой v0.9.5 такого кода нет, но её состояние переписывается при апгрейде — поэтому issue и воспроизводилась на обеих.