release v1.2.4
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.4The 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
- Download:
singbox-launcher-v1.2.4-macos.zip - Extract the ZIP file
- Remove quarantine attribute (required):
xattr -cr "singbox-launcher.app" && chmod +x "singbox-launcher.app/Contents/MacOS/singbox-launcher"
- Double-click
singbox-launcher.appto 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)
- Download:
singbox-launcher-v1.2.4-win64.zip - Extract the ZIP file to a folder, for example:
C:\Program Files\singbox-launcher\ - Run
singbox-launcher.exefrom that folder- You may need administrator rights to install to Program Files
- The launcher will automatically download
sing-boxandwintun.dllon first launch
Windows 7 (x86, legacy)
- Download:
singbox-launcher-v1.2.4-win7-32.zip - 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
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": nullin 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": nullon 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" asnil, but the merge step never honoured that meaning and copied thenilin as a live value, which the emitter then marshalled as literalnull. Two things made it nasty. First, thenilwas written intostate.jsonas a real JSONnulland 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 runsOutboundFieldDiffover legacy state on its own, so a urltest whose stored body lacked these keys got all three tombstoned automatically. Anulloverride 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.go—applyOutboundUpdatenow skipsnilvalues inpatch.Optionsinstead of merging them, so the key falls through totarget.Options. This is the read-side heal: thenulls already sitting in users'state.jsonstop reaching the emitter, without a migration pass rewriting anyone's file.core/build/outbound_diff.go—mapDiffno longer writes tombstones for keys present inbasebut absent fromform. With merge ignoring them, continuing to emit them would only accumulate dead weight instate.json; newly written patches are clean. Removing a key is expressed through the outbound type, not the patch: the selector branch inedit_dialog.goalready dropsurl/interval/toleranceon a urltest→selector switch and never depended on the tombstone.- Tests:
mapDiff/OutboundFieldDiffhad 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. Addedcore/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 inpreset_outbounds_test.go. core/config/varsubst.gois not implicated, despite looking related. It resolves@urltest_interval-style placeholders — a different, older bug (v0.8.8.1) — and it skipsnilvalues entirely, since it only touches values that type-assert tostring.
Notes
- Affects subscription/auto outbounds, not the local wizard's.
defaultLocalURLTestOptionsalways 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.go—applyOutboundUpdateтеперь пропускаетnil-значения вpatch.Options, а не мержит их, и ключ проваливается вtarget.Options. Это и есть исцеление на чтении:null, уже лежащие вstate.jsonу пользователей, перестают доезжать до эмиттера, и при этом ничей файл не переписывает миграция.core/build/outbound_diff.go—mapDiffбольше не пишет надгробия для ключей, которые есть в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 и воспроизводилась на обеих.