3.1.0 - 2026-06-07
Release Notes
TR-300 parity release: full Windows distribution + self-update matrix, a dns
subcommand, and a batch of speed-test stability fixes.
Added
- Four first-class Windows installers (MSI/EXE × Global/Corporate), each packaging both
nd300.exeandspeedqx.exe. A new.github/workflows/windows-installers.ymlbuilds the Corporate MSI (wix-corporate/corporate.wxs, bare WiX 3candle/lightwith-sice:ICE38/64/91), the Global EXE (inno/global.iss, perMachine/admin, system PATH), and the Corporate EXE (inno/corporate.iss, perUser/no-UAC, user PATH) via Inno Setup 6, plus a<hex> *<name>.sha256sidecar for each, and uploads all 6 add-on assets withgh release upload --clobber. The Global MSI continues to come fromwix/main.wxsvia cargo-dist. Corporate editions install to%LocalAppData%\Programs\nd300\bin\with no admin prompt; Global editions install toC:\Program Files\nd300\bin\. Pick one format per edition.- Main-push trigger (divergence from TR-300): ND-300's
Releaseworkflow fires on amainpush and creates the tag itself, sowindows-installers.ymltriggers onworkflow_run: workflows:["Release"] types:[completed]filtered toconclusion=='success' && head_branch=='main'(plusworkflow_dispatchwith ataginput that always runs). The tag is resolved by readingversionfromCargo.tomlat the triggering commit →v$version. A pre-flight probes the release fordist-manifest.json+ the Global MSI, and idempotently exits 0 when all 6 corporate/EXE assets are already attached (so no-op/docs main pushes don't rebuild).
- Main-push trigger (divergence from TR-300): ND-300's
nd300 dnssubcommand — bare-subcommand form of the existing-d/--dnsflag (src/cli.rs). Routed through the same semi-exit-early path insrc/main.rs(exit on failure, fall through to diagnostics on success), sond300 dns≡nd300 --dns.
Changed
- Self-update parity with TR-300 (
src/actions/update.rs). ND-300's existing async-reqwestflow and richer cargo shadow-cleanup machinery (ShadowCleanupDecision,classify_shadow_cleanup, the uninstall integration + tests) are preserved; the following capabilities were added on top:- Windows install-origin dispatch. A
HKCU\Software\ND300\InstallSourcemarker (written by each installer:msi-global/msi-corporate/exe-global/exe-corporate) is read byread_install_source_marker()(authoritative), with a path-basedclassify_install_path()fallback (\program files\nd300\→ MsiGlobal,\appdata\local\programs\nd300\→ MsiCorporate,\.cargo\bin\→ CargoOrInstaller).build_strategy_listreturns a single matching MSI/EXE strategy (no cross-fall-back between installer types). Four newUpdateStrategyvariants (MsiGlobal/MsiCorporate/ExeGlobal/ExeCorporate) with stablejson_ids. - SHA-256-verified in-place installer upgrade.
try_msi_install/try_exe_installdownload the matching installer to%TEMP%via async reqwest, fetch the.sha256sidecar,verify_checksum(refuse-on-mismatch via the testablechecksum_verdict), runmsiexec /i /passive /norestart(handling exit 3010 reboot-required honestly) or the EXE/SILENT /SUPPRESSMSGBOXES /NORESTART, then re-exec--versionto confirm the replacement landed. - Prerelease-aware versioning.
strip_prerelease_metadata+ an upgradedis_newercorrectly handle-rc/+metasuffixes. verify_cargo_post_installre-execs--versionaftercargo install(wired in after the shadow-cleanup success path); on a stale/crates.io-lag mismatch it falls through to the prebuilt installer instead of looping "update available."rustup_update_stable_best_effortbeforecargo install; explicit GitHub rate-limit (403) messaging viahttp_status_message.
- Windows install-origin dispatch. A
- Self-update
--jsonschema (Windows): every update payload now carries a top-level"install_origin"field (msi-global/msi-corporate/exe-global/exe-corporate/cargo-or-installer/unknown). The field is Windows-only (omitted on macOS/Linux). New"strategy"valuesmsi_global/msi_corporate/exe_global/exe_corporatejoin the existing set;"method"still maps tocargo/installer. Cargo.toml: version3.0.11→3.1.0;allow-dirty = ["ci", "msi"]; new[target.'cfg(windows)'.dependencies]sha2 = "0.10"+winreg = "0.52";/wix-corporate/**and/inno/**added to the crateincludelist.
Fixed
- M1 — speedqx is now bounded by per-request timeouts and an outer wall-clock cap. Each per-request builder in the Cloudflare, LibreSpeed, and fast.com download loops (and the fast.com upload loop) now sets
.timeout(remaining_budget(deadline))(floored at 1s), so a stalled CDN socket can't block a singleresp.bytes().awaitfor the full 120s client timeout and overrun the deadline-based loop.src/bin/speedqx.rsnow wraps the wholespeedtest::runin atokio::time::timeoutouter cap (scaled to the configured durations, ≥120s) and exits 2 with a clear message on timeout (nd300 already had a wall-clock cap; speedqx had none). - M2 —
main.rs's overallRUN_ALL_CAPnow scales with--speed-duration. The fixed 90s cap is replaced bymax(90s, 4 * speed_duration + 30s)(2 providers × 2 directions, plus headroom), so a deliberately long speed test (--speed-duration 60) isn't truncated and falsely reported as a "severely degraded network."--fastkeeps the 90s floor. - L1 — honest per-provider failure. Cloudflare, LibreSpeed, and fast.com now set
error = Some("no successful transfers")when discovery/latency succeeded but zero transfer samples were collected, so the speedqx table shows an explicit error row instead of a silent N/A (and aggregation, which filters onerror.is_none(), correctly excludes the provider). - L2 — speedqx honest exit code.
speedqxnow exits non-zero when no provider produced positive throughput in either direction, mirroringdiagnostics/speed.rs::determine_speed_status's "measured" check. A slow-but-working link (positive throughput) still exits 0. - L4 — NDT7 keeps earlier samples on a mid-run error. A mid-run iteration error in the NDT7 download/upload deadline loops now
breaks (retaining samples already collected) instead of?-collapsing the whole provider to an error. A first-iteration failure that collected nothing still reports an honest"no successful transfers"provider error.
Known limitations
- L3 (the cosmetic
Risk::Lowlabel onRestartNetworkServices) and L5 (fast.com token-scraping brittleness) are intentionally not addressed in this release — documented as known/low.
Install nd300 3.1.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.1.0/nd300-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.1.0/nd300-installer.ps1 | iex"Download nd300 3.1.0
| File | Platform | Checksum |
|---|---|---|
| nd300-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| nd300-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| nd300-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| nd300-x86_64-pc-windows-msvc.msi | x64 Windows | checksum |
| nd300-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| nd300-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
| nd300-x86_64-unknown-linux-musl.tar.xz | x64 MUSL Linux | checksum |