Skip to content

Releases: QubeTX/qube-network-diagnostics

3.4.0 - 2026-06-10

10 Jun 14:08

Choose a tag to compare

Release Notes

Comprehensive stability + accuracy hardening across all three subsystems
(diagnostics, speed test, fix flow), two new speed-test providers, and an
exhaustive technician mode (18 → 25 deep diagnostics).

Added

  • Two new speed-test providers (4 → 6 in speedqx):
    • M-Lab MSAK (src/speedtest/msak.rs) — the multi-stream throughput1
      successor to NDT7: two parallel WebSocket streams per direction measure
      aggregate capacity (NDT7's single flow structurally underestimates on
      high-BDP/lossy paths). Locate-API discovery, subprotocol
      net.measurementlab.throughput.v1, 500ms aggregate sampling, kernel
      TCPInfo MinRTT/RTT for ping/jitter. Same open M-Lab platform/policy as
      the existing NDT7 integration. Skip with --skip-msak.
    • Apple networkQuality (src/speedtest/applenq.rs) — the capacity half
      of the IETF responsiveness methodology against
      mensura.cdn-apple.com: 4 parallel HTTPS connections (large-object GETs /
      upload-sink POSTs) with 500ms aggregate sampling and 10-probe warmed
      latency. A fifth distinct CDN family — independent signal for the merge.
      Skip with --skip-apple. (Ookla Speedtest.net was evaluated and excluded:
      its EULA permits only personal, non-commercial use via the official CLI,
      with no third-party protocol authorization — rationale documented in
      applenq.rs.)
  • Bootstrap confidence intervals surfaced: the previously dormant
    percentile-method bootstrap (statistics::bootstrap_ci) now runs on the
    pooled per-direction samples and displays as 941 Mbps ±12 Mbps in speedqx
    and nd300 tech mode, with an additive confidence_intervals JSON field
    (suppressed below 8 pooled samples; deterministic via data-seeded PRNG).
  • Seven new deep diagnostics in technician mode (18 → 25 total):
    route_path (bounded traceroute with first-hop/ISP-boundary/largest-jump
    analysis), packet_loss (sustained 30-probe bursts × 3 anycast targets),
    nat (double-NAT + CGNAT 100.64/10 detection), dns_benchmark (system vs
    public resolver timing + NXDOMAIN-wildcard hijack probe + subprocess-free
    DNSSEC validation check), wifi (structured signal/channel/band/PHY/
    security/rates via netsh/airport/nmcli-or-iw), captive_portal
    (redirect-disabled probe), ntp (dependency-free SNTP clock-offset check
    with HTTP-Date fallback; >30s skew is flagged — it breaks TLS).
  • Partial results when diagnostics hit the wall-clock cap: run_all now
    enforces the cap internally and returns completed checks plus fabricated
    timed_out Fail rows for unfinished ones (exit code stays 2). JSON gains a
    top-level timed_out flag and per-row timed_out markers. JSON
    consumers keying on the old {"error":"timeout"} shape must migrate.
  • New shared diagnostics infrastructure: src/diagnostics/ping.rs (one home
    for the ping invocation/parsing previously triplicated), retry_probe /
    ping_budget / harvest_or helpers and a TRACE (60s) timeout class in
    diagnostics/util.rs, and src/speedtest/adaptive.rs (throughput-targeted
    request sizing).

Changed

  • Core diagnostic verdicts are de-flaked via consistent-failure requirements
    (single transient blips no longer flip verdicts):
    • Gateway: 3-ping burst + conditional second burst — Fail now requires 0/6
      replies; partial loss is a new Warn with loss %. Additive
      packets_sent/packets_received JSON fields.
    • DNS: three independent domains (dns.google, one.one.one.one,
      example.com) probed concurrently with one retry each; verdict on
      count-resolved + median time. One slow domain among fast peers no
      longer Warns. Additive resolution_tests JSON field.
    • Latency: 6 pings/target (was 4); average-based Warns require ≥2 reachable
      targets; exactly one reachable target reports partial reachability
      instead of letting that target's latency set the verdict.
    • Ports: two endpoints per port on independent operators (80/443/53 across
      1.1.1.1↔8.8.8.8; 22 across github.com↔gitlab.com), 2 attempts each — open
      if either connects, so one provider's outage no longer Warns. New
      outcome field (open/blocked/unresolved); unresolved ports are
      excluded from the verdict denominator; latency_ms is now connect-only.
  • Speed-test merge hardening: all samples flow through a non-finite
    sanitize choke point; unknown variance (1-sample providers, no-sample
    fallback values) is now treated as LEAST trusted in the inverse-variance
    merge (the old floor rule handed degenerate providers the highest weight);
    providers need ≥4 samples per direction to join the headline merge, with
    exclusions reported in an additive merge_exclusions JSON field and an
    Excluded display row. Merged numbers shift where degenerate providers
    were previously over-weighted (intended).
  • NDT7 sampling density: 500ms per-interval download samples and
    server-AppInfo-delta upload samples (~20 per 10s iteration vs exactly 1
    before); upload frame growth switched to the reference-client
    16×-bytes-sent rule (1MB frames after ~8MB vs ~101MB). Per-provider
    bandwidth_samples arrays are substantially larger — flag for any JSON
    consumer parsing them.
  • Adaptive request sizing: LibreSpeed download ckSize and the
    LibreSpeed/Cloudflare/fast.com upload payloads now target ~2s per request
    at the last measured throughput (LibreSpeed's fixed 100MB chunks yielded
    0–2 samples per 30s window on slower lines).
  • fast.com: latency upgraded from one unloaded HEAD probe to a 10-probe
    warmed burst (min + RFC 3550 jitter — jitter was always null before);
    upload give-up softened to 5 consecutive failures with a small-payload
    retry. LibreSpeed server selection probes 30 candidates (was 5) and the
    hardcoded fallback list is now 9 geo-diverse servers (was 3 EU/US-East).
  • Real bufferbloat measurement (tech mode): ping bursts run during
    sustained multi-stream download/upload saturation phases with per-direction
    A+–F grades (the old implementation admitted its loaded number was an
    estimate). Now honors --fast. Real path-MTU discovery (DF-bit binary
    search) joins the MTU section; IPv6 gains an HTTPS-over-v6 fetch and a
    v4-vs-v6 connect comparison; proxy gains PAC reachability + WPAD detection;
    ARP gains gateway-presence/duplicate-MAC health analysis.
  • nd300 fix evidence quality:
    • The loop's diagnostic passes force-skip the speed test (no action targets
      Speed — pinned by a registry invariant test; a speed-only failure now
      reports cleanly instead of looping to Exhausted, and the freed budget
      funds the confirmation pass below).
    • A failing baseline is re-confirmed with a second diagnostic pass
      before the first repair plan; only failures present in both passes are
      acted on. Transient blips self-clear (outcome Fixed, zero actions);
      flickering failures are recorded as intermittent (report section + JSON
      intermittent_failures).
    • Effectiveness attribution fixed: a cleared failure is credited only to
      the most recent successful action that iteration whose declared targets
      contain it (previously every successful action got credit for every
      cleared key, letting natural recoveries bias planning).
    • Re-disabling a VPN after re-enable now requires two consecutive failed
      connectivity checks.
  • --speed-duration default raised 10 → 15 seconds (the run_all cap formula
    is unchanged: 4×15+30 = the existing 90s floor); tech mode's cap gains a
    +150s deep-probe budget. speedqx's six providers run sequentially —
    a full default run is now ~5.5 minutes (use the skip flags to trim).

Removed

  • The legacy fixed Stage 1/2/3 orchestrators (run_stage1/2/3), their DNS
    fallback handlers, wait_for_connectivity/verify_dns_stability, and the
    StepResult type (~650 lines) — unused since the v3.0 triage loop. All
    platform primitives used by the action registry and restore machinery are
    kept; the captive-portal probe URL is now a shared named constant.

Fixed

  • A latent budget bug where multi-probe ping bursts could be truncated by the
    fixed 10s subprocess cap (budgets now scale with probe count via
    ping_budget).
  • The aggregate latency fallback can no longer be set by a single noisy
    unloaded probe (jitter-bearing providers are preferred).
  • A 21-way tokio::join! of inlined futures overflowed the Windows
    main-thread stack in tech mode; module futures are now boxed.

Tests

  • ~110 new unit tests: pure verdict functions for every reworked core module,
    per-OS canned-transcript parsers (ping/tracert/traceroute/netsh/airport/
    nmcli/iw/dig), merge/CI statistics, SNTP packet math, fix-loop scenarios
    via a new scripted-diagnostics seam (DiagProbe), and effectiveness
    attribution rules.

Install nd300 3.4.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.4.0/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.4.0/nd300-installer.ps1 | iex"

Download nd300 3.4.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-...
Read more

3.3.2 - 2026-06-08

09 Jun 00:18

Choose a tag to compare

Release Notes

Build/release-infra hardening only — no changes to the nd300/speedqx binaries.

Fixed

  • cargo-dist install is no longer blocked by GitHub's flaky installer-script CDN.
    ci.yml's dist-plan job and release.yml's plan job now fetch the prebuilt
    dist binary tarball (cargo-dist-x86_64-unknown-linux-gnu.tar.xz, with
    curl --retry) and extract dist directly, instead of piping
    cargo-dist-installer.sh | sh. GitHub's release CDN had repeatedly returned
    504 on that small script asset while the .tar.xz stayed available — and
    because dist-plan is not continue-on-error, it failed the whole CI run and
    blocked the crates.io publish (it held up the 3.3.1 release for hours). The
    per-target build-local-artifacts jobs still use cargo-dist's platform-detecting
    installer for their own runners (each needs a different host tarball).

Install nd300 3.3.2

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.3.2/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.3.2/nd300-installer.ps1 | iex"

Download nd300 3.3.2

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

3.3.1 - 2026-06-08

08 Jun 23:38

Choose a tag to compare

Release Notes

Dependency security pass — clears every RUSTSEC vulnerability flagged by the
CI cargo-audit job. No changes to the nd300/speedqx binaries' behavior.

Security

  • rustls-webpki 0.103.9 → 0.103.13 (lockfile-only) clears four advisories in
    the real TLS path (rustls/reqwest/tokio-tungstenite): RUSTSEC-2026-0049
    (CRLs not treated as authoritative by Distribution Point), RUSTSEC-2026-0098
    (URI-name constraints incorrectly accepted), RUSTSEC-2026-0099 (wildcard-name
    constraints accepted), RUSTSEC-2026-0104 (reachable panic in CRL parsing).
  • quinn-proto 0.11.13 → 0.11.14 (lockfile-only) clears RUSTSEC-2026-0037
    (DoS in Quinn endpoints). quinn is reqwest's optional http3 stack, which
    ND-300 never enables or compiles
    — the entry exists only because cargo-audit
    scans Cargo.lock, not the build graph — but it is bumped so the advisory clears.

Changed

  • indicatif 0.17 → 0.18 — replaces the unmaintained number_prefix
    (RUSTSEC-2025-0119) with the maintained unit-prefix, clearing that warning.
    Pulls console 0.16; progress-bar output is visually identical (the used API —
    ProgressBar/ProgressStyle spinners and bars — is unchanged). Also refreshes
    the lockfile rand 0.9.2 → 0.9.4.

Added

  • .cargo/audit.toml documenting the two remaining non-vulnerability
    advisories that cannot be cleanly dropped, each with rationale (the CI audit job
    is now clean — 0 vulnerabilities, the two below ignored by ID):
    • RUSTSEC-2024-0436 (paste, unmaintained) — a build-time proc-macro pulled
      transitively by default-net 0.22's netlink stack (Linux default-gateway
      detection). No runtime risk; removable only by migrating off the now-renamed/
      abandoned default-net to its successor netdev, a cross-platform
      gateway-detection change out of scope here (tracked separately).
    • RUSTSEC-2026-0097 (rand, unsound) — only manifests with a custom global
      logger calling rand::rng(), which ND-300 never does; the flagged rand is the
      never-compiled ghost behind reqwest's disabled http3/quinn, and no patched
      release clears it yet.

Install nd300 3.3.1

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.3.1/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.3.1/nd300-installer.ps1 | iex"

Download nd300 3.3.1

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

3.3.0 - 2026-06-08

08 Jun 05:55
f800c05

Choose a tag to compare

Release Notes

Build + deploy cycle made consistent with the sibling TR-300 tool, plus a LICENSE
file. No changes to the nd300/speedqx binaries themselves.

Added

  • LICENSE file (PolyForm Noncommercial 1.0.0) at the repo root — matches the
    Cargo.toml license field and the sibling TR-300 repo (ND-300 previously
    declared the SPDX license but shipped no file). Added to the crate include list
    and shown in the Inno EXE installer wizards (LicenseFile=..\LICENSE in
    inno/global.iss + inno/corporate.iss, replacing the prior omission).
  • .github/workflows/crates-publish.yml — publishes the nd300 crate via
    workflow_run after CI succeeds on a main push (idempotent; skips an
    already-published version). Mirrors TR-300.

Changed

  • Deploy model → tag-push, consistent with TR-300. release.yml is now the
    stock cargo-dist tag-triggered workflow (+ the nd-300-installer legacy
    aliases); it no longer fires on a main push and no longer publishes the crate
    (that moved to crates-publish.yml, avoiding a double-publish). windows-installers.yml
    reverts to the tag-based filter (startsWith(head_branch,'v'), tag resolved from
    head_branch). A release is now: bump version → merge to main (crate
    publishes) → push a vX.Y.Z tag (binaries/installers/GitHub Release).
  • Docs (CLAUDE.md, AGENTS.md): rewritten Release Process as the canonical
    tag-push deploy guide (incl. bumping Cargo.toml + the homepage version
    fallbacks), updated patch-bump loop, and the windows-installers.yml note.

Install nd300 3.3.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.3.0/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.3.0/nd300-installer.ps1 | iex"

Download nd300 3.3.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

3.2.1 - 2026-06-08

08 Jun 01:26
ede79d8

Choose a tag to compare

Release Notes

Fixed

  • Windows EXE installer build (inno/global.iss). The v3.2.0 consolidation work referenced a non-existent Inno Setup constant {userprofile} in the Global EXE's [Run] parameters, which failed the iscc step in windows-installers.yml — so the v3.2.0 GitHub release published without the Corporate MSI and the two Inno EXE add-ons (22 of 28 assets). Inno has no reliable pre-elevation user-profile constant ({userprofile} doesn't exist; {%USERPROFILE} / runasoriginaluser resolve to the admin account under separate-credential elevation), so the --user-profile argument is dropped from the Global EXE's migrate-cleanup invocation. migrate-cleanup falls back to the process environment (CARGO_HOME, then %USERPROFILE% / %LocalAppData%) — correct in the common case (an admin elevating their own session) and fail-safe otherwise (no copy under the admin profile → harmless no-op). The perMachine MSI still resolves the invoking user via its Impersonate='yes' custom action. Behavior is otherwise unchanged; v3.2.1 publishes the complete 28-asset Windows installer set.

Install nd300 3.2.1

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.2.1/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.2.1/nd300-installer.ps1 | iex"

Download nd300 3.2.1

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

3.2.0 - 2026-06-07

08 Jun 01:00
e4a5060

Choose a tag to compare

Release Notes

Cross-method install cleanup: consolidate to a single nd300/speedqx install
(one version, one edition) — interactively from the installers AND on a silent
self-update. Windows-only behavior; macOS/Linux already overwrite the single
~/.cargo/bin copy and are unaffected.

Added

  • Hidden nd300 migrate-cleanup subcommand (src/actions/migrate.rs, #[command(hide = true)] in src/cli.rs). Detects and removes duplicate installs by reusing the existing tested deletion primitives — it does not re-implement deletion:
    • From src/actions/uninstall.rs: uninstall_path, CleanupReport, is_sole_package_in_dir, and the OUR_BINARIES allowlist (now pub(crate)).
    • From src/actions/update.rs: cargo_bin_dir, same_path, classify_install_path, current_install_shadows_cargo_install, current_exe_real_path, classify_shadow_cleanup, ShadowCleanupDecision, InstallOrigin (now pub(crate)).
    • Flags: --cargo-copy, --other-edition, --quiet, --dry-run, --json, --user-profile <path>, --cargo-home <path>. With no target flag, defaults to --cargo-copy only.
    • Targets: the shadowing older cargo install copy in ~\.cargo\bin, and the other Windows edition (Global perMachine C:\Program Files\nd300\bin ↔ Corporate perUser %LocalAppData%\Programs\nd300\bin). --cargo-home/--user-profile take precedence over %USERPROFILE%/process env so a perMachine installer can resolve the invoking user's .cargo.
    • Hard safety guarantees (unit-tested): only ever deletes nd300.exe/speedqx.exe (allowlist); never cargo.exe/rustup.exe/non-allowlisted files; never removes the .cargo\bin PATH entry (delegated to uninstall_path, which only edits PATH when is_sole_package_in_dir is true — and .cargo\bin always also holds cargo/rustup); never touches ~/Downloads; never deletes the running install (same_path guard); never escalates privileges (a target needing admin we lack is reported needs admin: <path> and continues — exit 0); refuses shell-unsafe paths (via uninstall_path's existing build_delayed_delete_command refusal).
    • Exit 0 even on partial/empty/needs-admin — cleanup is advisory and must never fail an installer. Only a true internal error (couldn't determine the running install's own location) is nonzero.
    • Reporting: human (removed / scheduled-on-exit / skipped:<reason> / needs-admin / failed, per target) and --json (stable status values removed/scheduled/would_remove/skipped/needs_admin/failed).
  • Installer consolidation UX + invocation on all four Windows installers. Both options default ON, so a SILENT install consolidates too:
    • Inno (inno/global.iss, inno/corporate.iss): two default-checked [Tasks] (cleancargo, cleanotheredition) + a [Run] postinstall calling {app}\bin\nd300.exe migrate-cleanup --quiet <flag> with Flags: runhidden waituntilterminated, conditioned on each task. Added AppMutex=ND300_Running + CloseApplications=yes. Under /SILENT (the self-updater's EXE path) default-checked tasks fire automatically — both cleanups run with no /MERGETASKS suppression. The perMachine Global EXE passes the invoking user's profile via --user-profile "{userprofile}" (Inno runasoriginaluser is unreliable under right-click-Run-as-admin); the perUser Corporate EXE runs as the user (no --user-profile).
    • WiX (wix/main.wxs Global perMachine, wix-corporate/corporate.wxs Corporate perUser): CLEANCARGO/CLEANOTHEREDITION properties (default 1, Secure='yes') + a ConsolidateDlg WixUI dialog (two pre-checked checkboxes inserted between WelcomeDlg and CustomizeDlg) + two deferred, Impersonate='yes', Return='ignore' custom actions scheduled Before='InstallFinalize', conditioned NOT Installed AND CLEAN…="1" (install/upgrade only, not uninstall/repair). The CAs use FileKey='exe0' + ExeCommand (NOT WixUtilExtension's WixQuietExec) because cargo wix / the bare candle+light build only link WixUIExtension — a WixQuietExec CA would fail the CI link step. Impersonate='yes' makes the perMachine MSI run cleanup as the invoking user, so the user's .cargo/%LocalAppData% resolve without needing --user-profile.

Changed

  • Silent self-update now consolidates. No code change was required in src/actions/update.rs: try_msi_install already runs msiexec /i … /passive /norestart (the default-1 CLEANCARGO/CLEANOTHEREDITION properties stand), and try_exe_install already runs the EXE /SILENT … with no /MERGETASKS suppression (the default-checked tasks fire). Verified both paths leave the cleanups enabled.
  • Cargo.toml: version 3.1.03.2.0.
  • Several uninstall.rs / update.rs items widened from private to pub(crate) so migrate.rs can reuse them (no behavior change to update/uninstall).

Notes

  • macOS/Linux compilation, and the WiX/Inno installer builds, are validated only in CI / on a Windows runner — the cross-platform Rust targets and candle/light/iscc are not run locally. All v3.1.0 update/uninstall behavior and tests are preserved.

Install nd300 3.2.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.2.0/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.2.0/nd300-installer.ps1 | iex"

Download nd300 3.2.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

3.1.0 - 2026-06-07

07 Jun 22:28
d154724

Choose a tag to compare

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.exe and speedqx.exe. A new .github/workflows/windows-installers.yml builds the Corporate MSI (wix-corporate/corporate.wxs, bare WiX 3 candle/light with -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> .sha256 sidecar for each, and uploads all 6 add-on assets with gh release upload --clobber. The Global MSI continues to come from wix/main.wxs via cargo-dist. Corporate editions install to %LocalAppData%\Programs\nd300\bin\ with no admin prompt; Global editions install to C:\Program Files\nd300\bin\. Pick one format per edition.
    • Main-push trigger (divergence from TR-300): ND-300's Release workflow fires on a main push and creates the tag itself, so windows-installers.yml triggers on workflow_run: workflows:["Release"] types:[completed] filtered to conclusion=='success' && head_branch=='main' (plus workflow_dispatch with a tag input that always runs). The tag is resolved by reading version from Cargo.toml at the triggering commit → v$version. A pre-flight probes the release for dist-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).
  • nd300 dns subcommand — bare-subcommand form of the existing -d/--dns flag (src/cli.rs). Routed through the same semi-exit-early path in src/main.rs (exit on failure, fall through to diagnostics on success), so nd300 dnsnd300 --dns.

Changed

  • Self-update parity with TR-300 (src/actions/update.rs). ND-300's existing async-reqwest flow 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\InstallSource marker (written by each installer: msi-global/msi-corporate/exe-global/exe-corporate) is read by read_install_source_marker() (authoritative), with a path-based classify_install_path() fallback (\program files\nd300\ → MsiGlobal, \appdata\local\programs\nd300\ → MsiCorporate, \.cargo\bin\ → CargoOrInstaller). build_strategy_list returns a single matching MSI/EXE strategy (no cross-fall-back between installer types). Four new UpdateStrategy variants (MsiGlobal/MsiCorporate/ExeGlobal/ExeCorporate) with stable json_ids.
    • SHA-256-verified in-place installer upgrade. try_msi_install / try_exe_install download the matching installer to %TEMP% via async reqwest, fetch the .sha256 sidecar, verify_checksum (refuse-on-mismatch via the testable checksum_verdict), run msiexec /i /passive /norestart (handling exit 3010 reboot-required honestly) or the EXE /SILENT /SUPPRESSMSGBOXES /NORESTART, then re-exec --version to confirm the replacement landed.
    • Prerelease-aware versioning. strip_prerelease_metadata + an upgraded is_newer correctly handle -rc/+meta suffixes.
    • verify_cargo_post_install re-execs --version after cargo 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_effort before cargo install; explicit GitHub rate-limit (403) messaging via http_status_message.
  • Self-update --json schema (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" values msi_global/msi_corporate/exe_global/exe_corporate join the existing set; "method" still maps to cargo/installer.
  • Cargo.toml: version 3.0.113.1.0; allow-dirty = ["ci", "msi"]; new [target.'cfg(windows)'.dependencies] sha2 = "0.10" + winreg = "0.52"; /wix-corporate/** and /inno/** added to the crate include list.

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 single resp.bytes().await for the full 120s client timeout and overrun the deadline-based loop. src/bin/speedqx.rs now wraps the whole speedtest::run in a tokio::time::timeout outer 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 overall RUN_ALL_CAP now scales with --speed-duration. The fixed 90s cap is replaced by max(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." --fast keeps 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 on error.is_none(), correctly excludes the provider).
  • L2 — speedqx honest exit code. speedqx now exits non-zero when no provider produced positive throughput in either direction, mirroring diagnostics/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::Low label on RestartNetworkServices) 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 | sh

Install 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

3.0.11 - 2026-06-07

07 Jun 08:29
ab95be2

Choose a tag to compare

Release Notes

Fixed

  • M5 — a completely failed speed test now reports failure (exit 2) instead of a misleading "too slow" warning. When every speed-test provider errors out (or returns zero throughput), diagnostics/speed.rs previously fell through determine_speed_status to the slow-link branch, producing a 0.00 Mbps down / up aggregate, a "Download too slow for most activities" warning, and exit 1. determine_speed_status now first checks whether any provider succeeded (no error) and reported positive throughput in either direction; if not, it returns the new SpeedStatus::Failed, which check() maps to a Fail-status diagnostic with an honest standalone summary ("Speed test failed — no provider returned a result", with no fabricated 0.00 Mbps line). A genuinely slow-but-working link (e.g. 0.3 Mbps from a successful provider) is unaffected — the positive-throughput guard keeps it Poor → Warn.
  • L1 — the NDT7 speed test's per-direction safety timeout now actually fires. The NDT7 download loop (speedtest/ndt7.rs) re-armed a fresh 30s tokio::time::sleep(SINGLE_TEST_TIMEOUT) on every tokio::select! iteration, so the safety cap could never elapse and was dead code. It is now a single pinned absolute deadline (Instant::from_std(start + SINGLE_TEST_TIMEOUT)) computed once before the loop and driven with sleep_until, mirroring the existing per-session deadline arm. The upload loop, which previously had no safety cap at all, gets a symmetric pinned cap so a wedged write.send is now bounded.

Changed (be aware if you script against the tool)

  • The exit code for a fully-failed speed test (every provider errored / zero throughput) is now 2 (Fail) instead of 1 (Warn), and the Speed diagnostic's summary is Speed test failed — no provider returned a result rather than a 0.00 Mbps … too slow line. A slow-but-working connection is unchanged (still Warn / exit 1).

Behind the scenes

  • L5 — system-info lookups in the core diagnostics no longer briefly block the async runtime. The synchronous, potentially-blocking sysinfo::Networks::new_with_refreshed_list() and default_net::get_default_gateway() calls in diagnostics/gateway.rs, diagnostics/public_ip.rs, diagnostics/interfaces.rs, and diagnostics/shared_cache.rs are now wrapped in tokio::task::spawn_blocking, returning owned Send data with a JoinError fallback to the pre-existing empty/None result (never an unwrap panic). interfaces.rs extracts owned per-interface fields (name, MAC bytes, IPs, rx/tx) inside the blocking closure and keeps its async, timeout-wrapped get_wifi_summary() call outside it; the is_up semantics (has an IP && rx > 0) are byte-for-byte preserved. Mirrors the existing spawn_blocking pattern in actions/fix/adapters.rs and diagnostics/vpn.rs.

Known limitations

  • L2 (deferred) — localized (non-English) Windows. On non-English Windows display languages, some deep-diagnostic modules that parse localized netsh / ipconfig / netstat output may return empty or partial results, because they match English labels and decode console output as UTF-8. This is tracked for a future locale-invariant parsing pass and is not addressed in this release.

Install nd300 3.0.11

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.0.11/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.0.11/nd300-installer.ps1 | iex"

Download nd300 3.0.11

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

3.0.10 - 2026-06-07

07 Jun 08:00
c4e6666

Choose a tag to compare

Release Notes

Fixed

  • M1 — cargo install nd300 can no longer fail on a read-only / hardened / sandboxed build cache. build.rs previously wrote the generated man pages into CARGO_MANIFEST_DIR/man/ with .unwrap()'d fs::create_dir_all + fs::write calls. On the primary install path (cargo install nd300), that directory is the registry extraction under …/registry/src/…, which is read-only on locked-down machines — the failing write would panic the build script and abort the install. build.rs now (a) renders both man pages into in-memory buffers (the only step that keeps its panic, since a failed render is a genuine bug), (b) writes OUT_DIR/man/*.1 strictly best-effort (create_dir_all(...).is_ok() guard + let _ = fs::write(...)), and (c) writes the committed repo man/*.1 only when a new is_dev_source_tree(CARGO_MANIFEST_DIR) check confirms the build is happening in a live source checkout (i.e. the manifest path is not under /registry/src/, /registry/cache/, /git/checkouts/, or /vendor/), and even then best-effort. Net: cargo install can never panic on a man-page write, while a normal local cargo build still refreshes the committed man/*.1 (so the dev tree stays in sync). Man pages still ship in release archives and the published crate via Cargo.toml's include = ["man/**"]; nothing reads OUT_DIR/man.
  • H5 — Windows uninstall/update no longer reports binary removal before it actually happens (and no longer defeats the updater's shadow guard). On Windows the running .exe can't be deleted in place, so the uninstaller spawns a background cmd /C … del that removes it after the process exits. Previously uninstall_path set binary_removed = true immediately on spawn — a false "removed" that, because the updater reuses uninstall_path for shadow-cleanup (actions/update.rs), silently bypassed the updater's Fatal "old install still shadows the new cargo binary" guard. CleanupReport now carries a distinct binary_removal_scheduled flag: the Windows branch sets it (and leaves binary_removed = false) on a successful spawn. uninstall reports "scheduled for removal (completes when this process exits)" and treats binary_removed || binary_removal_scheduled as success. The updater's cleanup_shadowing_current_install_after_cargo_success now returns Ok with an honest warning when removal was only scheduled (the new cargo binary wins in a new shell; manual cleanup instructions are printed if it doesn't), instead of either a silent success or a false Fatal. cleanup_current_install_for_cargo_retry stays Fatal when the file isn't actually gone — an immediate cargo retry needs it removed now — but now emits explicit "close this nd300 process and re-run nd300 update" instructions for the scheduled case.
  • L3 — Windows uninstall now fully cleans the PATH entry even with a trailing slash. remove_from_user_path now compares each PATH entry against the install dir ignoring case and a trailing \// (via a new pure path_entry_matches_target predicate, mirroring same_path in actions/update.rs), so an entry like C:\…\bin\ is removed instead of left behind as a dead PATH stub. Only the comparison is normalized; retained entries keep their original spelling.
  • L4 — the Windows delayed-delete command is now hardened against shell metacharacters. The cmd /C … del "<path>" string is built by a new build_delayed_delete_command helper that refuses (returns None, reported as a failure with an explanatory note) any path containing ", a newline/carriage-return, or a cmd metacharacter (& ^ | < >) — all of which are already illegal in real Windows paths — and escapes % to %% so cmd's environment-variable expansion can't mangle a path with a literal %.

Changed (be aware if you script against the tool)

  • nd300 uninstall --json gains an additive boolean field binary_removal_scheduled. The existing binary_removed field keeps its literal "is the binary gone right now?" meaning (so it now reads false on a successful Windows uninstall, where deletion completes on process exit), while success and the exit code key off binary_removed || binary_removal_scheduled. The field is purely additive and non-breaking on macOS/Linux, where it is always false and binary_removed is unchanged.

Install nd300 3.0.10

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.0.10/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.0.10/nd300-installer.ps1 | iex"

Download nd300 3.0.10

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

3.0.9 - 2026-06-07

07 Jun 07:12
7f6c442

Choose a tag to compare

Release Notes

Added

  • nd300 fix is now safe to cancel, time out, or crash mid-repair. A new restore registry (actions/fix/session.rs) records the inverse of every destructive change before it is applied, and loop_runner::run_and_finalize now races the triage loop against Ctrl-C (tokio::signal::ctrl_c) and wraps it in catch_unwind. On every terminal path — normal completion, user-declined, wall-clock cap, Ctrl-C, or a caught panic — the registry is drained (bounded by a 90s outer cap plus a 30s per-op cap) so any half-applied network state is rolled back. Ctrl-C now exits 130 via the new FinalOutcome::Interrupted; a caught panic restores state first, then re-raises as exit 101. The registry uses a non-poisoning tokio::sync::Mutex so a panic mid-loop never wedges cleanup.

Fixed

  • H2 — Consumer VPNs are now re-enabled instead of being left off silently. apply_disable_consumer_vpns (actions/fix/action.rs) registers a ReEnableVpn restore op per disabled VPN before reporting success and wires up the previously-dead vpn::offer_reenable (now reachable). If the run is interrupted before the offer, the drain reconnects each still-registered VPN. Enterprise VPNs are still never auto-disabled, and JSON/non-interactive runs still skip VPN disabling entirely.
  • H3 — A macOS deep reset can no longer strand the Mac with no network service. stage3_macos (actions/fix/stages.rs) registers a RecreateMacosService restore op (capturing iface/service/snapshot) before -removenetworkservice. Recreate now goes through a shared recreate_and_restore_macos_service helper that retries the create once and then restores the captured DNS/proxy/service-order/IPv4/IPv6 snapshot. If recreate still fails, the op is left registered (so the terminal drain retries it) and a manual-recovery message is returned, instead of returning early and skipping the snapshot restore.
  • H4 — An interface bounce no longer leaves the adapter disabled if re-enable fails. apply_bounce_interface registers a ReEnableInterface op before disabling and re-enables with a retry-once (porting the legacy Stage 2 2s-wait retry). If both attempts fail, the op stays registered for the drain and the action returns a loud, actionable message including the exact platform command to bring the adapter back up.
  • M3 — Linux stage3_linux no longer claims "Deleted profile" when the delete failed. The result of nmcli connection delete is now checked; on failure the function returns an error with the nmcli stderr instead of unconditionally recreating on top of the still-present profile.

Changed (be aware if you script against the tool)

  • nd300 fix --json output gains two fields: interrupted (bool) and manual_recovery_needed (array of human-readable strings for anything the drain could not restore). A new outcome value "interrupted" is emitted on Ctrl-C / caught panic, with exit_code 130.
  • The fix report (~/Downloads/nd300-fix-report-*.md) gains a prominent "⚠️ Manual recovery needed" section listing any restore the drain could not complete.

Behind the scenes

  • The pre-existing diagnostics/util.rs unit test nanosecond_resolve_times_out_to_none (added in 3.0.8) is timing-dependent and can flake on fast machines when the resolver answers within the 1-nanosecond budget; it was left unchanged here to keep this release scoped to the fix flow, and verified to be a pre-existing flake on the prior release rather than a Phase 2 regression.

Install nd300 3.0.9

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.0.9/nd300-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/QubeTX/qube-network-diagnostics/releases/download/v3.0.9/nd300-installer.ps1 | iex"

Download nd300 3.0.9

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