Release Notes
Highlights
- Run multiple VPNs at the same time. Connect to several WireGuard / OpenVPN profiles concurrently; one owns the kernel default route (the primary), the rest are split tunnels reachable on their declared
AllowedIPs. - Friendlier kill switch. Modes are now
off,block-on-drop, andvpn-only— the same words in the TUI, the CLI, and the JSON output. Thevpn-onlymode actually stays engaged whether the VPN is up or down (the v0.3.xAlwaysOnmode sat unarmed while the VPN was up, leaving a leak window between a drop and reconnect). - systemd-resolved-native DNS on Linux (#190). On distros where resolved manages DNS (Arch / Omarchy, NixOS-with-resolved, default Fedora Workstation), vortix now registers per-link DNS via
resolvectldirectly — nosystemd-resolvconf/openresolvshim package required. - OpenVPN inline 2FA / static-challenge (#191). Profiles with
static-challenge "<prompt>" 1now prompt for the TOTP/PIN at connect time and feed it via the OpenVPN management socket. TUI gets a 3-field form-style auth overlay; CLI prompts inline. - Polished Security Guard panel. New
Identity/Defenselayout, calmer colour treatment, and the encryption row now grades your cipher (modern AEAD,strong,deprecated, orINSECURE) instead of just printing the name.
Added
- Connect to multiple VPN profiles concurrently. Each gets its own retry budget and reconnect schedule.
- Auto-adopt: tunnels you started outside vortix (e.g.
wg-quick up corpfrom another terminal) appear in the TUI within a second. - Takeover overlay: when a second tunnel wants the default route, choose
[S]witch(replace),[B]oth(keep both up, the new one becomes the exit), or[N]o. - Auto-promote banner: if the primary drops and a secondary takes over, a one-line banner explains what happened and offers
[u]to revert. - Cipher strength annotation on the Security Guard
Encryptionrow, with bright alarms forINSECUREciphers (BF-CBC, DES, RC4, NULL, CAST5, IDEA, RC2). vortix up <name> --yesto skip the takeover prompt for scripts and CI.- Multi-tunnel keybindings:
Shift+D(disconnect every active tunnel with confirm),c(cancel an in-flight connect from Connection Details),B(Both from the takeover overlay),u(revert auto-promote). - JSON status reports every connected tunnel in
data.connections[]plusdata.primary. The legacydata.connectionfield stays populated when only one tunnel is up (back-compat for v1 consumers). - Sigil legend (
✓ ✗ ⚠ ─) in the?help overlay. - OpenVPN inline 2FA / static-challenge support (#191). Profiles that declare
static-challenge "<prompt>" 1now prompt for the TOTP/PIN at connect time and feed it to the server via the OpenVPN management socket using the SCRV1 envelope (SCRV1:base64(password):base64(otp)). Works for both TUI (3-field auth overlay) and CLI (vortix up <profile>adds a masked OTP prompt below the password). The static-challenge0variant (cleartext echo) is intentionally unsupported. - Auth overlay redesigned as a form: fixed-width label column, single
▸focus marker, andUp/Downarrows now cycle between Username / Password / OTP / Save-checkbox in a circular loop (Tab/BackTab still work for muscle-memory). Empty values render an em-dash placeholder; passwords and OTPs mask to filled-circle dots. - systemd-resolved DNS integration on Linux (#190). When
is_systemd_resolved()is detected andresolvectlworks, vortix callsresolvectl dns <iface> <ips>(andresolvectl domain <iface> ~.for primary tunnels) directly afterwg-quick upsucceeds. The connect path stripsDNS = …from the wg-quick-fed temp config so wg-quick never tries its own resolvconf path. Result: a fresh Arch / Omarchy / NixOS-with-resolved / default-Fedora host now connects WG-with-DNS without needing thesystemd-resolvconforopenresolvshim package — the historic "Missing dependencies: resolvconf (systemd)" wall is gone. Secondary tunnels also get per-link DNS registered (non-authoritative — DNS reachable on the link but doesn't claim the catchall), strictly better than v0.3.x's "strip-and-discard" behaviour.
Changed
- Breaking — CLI killswitch verbs.
vortix killswitch off | block-on-drop | vpn-onlyreplacesoff | auto | always | always-on. The old verbs are no longer accepted; the parser rejects them withUse: off, block-on-drop, vpn-only. - Breaking — JSON killswitch values.
data.security.killswitch_modenow emitsoff/block-on-drop/vpn-onlyinstead ofoff/auto/alwayson.data.security.killswitch_stateemitsInactive/Watching/Blockinginstead ofdisabled/armed/blocking. Scripts parsing the old values need to switch. - Kill switch
vpn-onlystays engaged whether the VPN is up or down. In v0.3.x theAlwaysOn + Connectedcombination resolved toArmedwith no actual firewall enforcement, so a drop between checks could leak. Now: default-DROP egress + per-tunnel ACCEPT rules are in place at all times when this mode is selected. vortix downwith no profile argument now disconnects every active tunnel (was single-tunnel only).vortix down <name>keeps the per-tunnel behaviour.vortix reconnectcycles every currently-connected tunnel.- Telemetry switched from
reqwest/curl/pingshell-outs to in-process HTTP (ureq) + raw-ICMP (socket2). Smaller binary, faster startup, no transient child processes. - Interface and process lookups on Linux / macOS go through
libcdirectly (getifaddrs,sysctlbyname,kill) instead of parsingip addr show/ifconfig/psoutput. Fewer locale-dependent parser bugs. - Default OpenVPN connect timeout bumped from 20s → 35s to accommodate the static-challenge MFA flow (TLS handshake + PAM verification +
PUSH_REPLYcan comfortably exceed 20s on geographically distant servers). - MSRV bumped from 1.75 to 1.85. A transitive dep (
idna_adapter) requiresedition2024, which Rust 1.75 doesn't support. Distros shipping older Rust — notably Ubuntu 24.04's apt — will needrustupfor source builds;curl | shinstaller users get a prebuilt binary as before. - Missing-dependency error formatting. When the WG dep-check fires,
wgandwg-quicknow report under a single label (wireguard-tools) and produce one install hint instead of duplicating the per-distro lines. Same for OpenVPN — gets a proper three-distro hint instead of falling through to the apt-or-dnf-only fallback. Vestigialcurlbinary check removed from startup (telemetry has been in-process HTTP since theureq/socket2switch above).
Fixed
- CLI's
vortix up <name>now refuses pre-2.4 OpenVPN before attempting to connect (matches what the TUI already did in v0.3.x; pre-fix the CLI would proceed and could leak pushed DNS through the primary's resolver). - TUI no longer freezes when connecting to OpenVPN — even on misbehaving / slow / broken servers. Four interacting bugs would each park the UI thread for seconds at a time on a connect; together they could lock the panel for 30+ seconds and queue keystrokes. Fixed by removing every synchronous subprocess call from the UI thread's connect-success path:
openvpn --daemonforks + detaches, but the daemonized grandchild inherited vortix's stdout/stderr pipes —wait_with_output()blocked forever waiting for pipe EOF that never came. Subprocesses can now declare themselves as daemonizing; the runner routes their stdio to/dev/nulland useschild.wait()instead.openvpn --versiondependency probe ran synchronously on the UI thread with no timeout. A slow first-run probe (Gatekeeper / Spotlight / antivirus on macOS) froze the panel until it returned. The probe now has a 10-second cap.route get default/ip route show defaultran inline on the UI thread every time the registry'srecompute_primaryfired (which is every connect, every disconnect, and every scanner tick). Right after a VPN claims the default route, the macOS kernel takes up to 30 seconds to answer that query. The query now lives in the scanner's background thread; its result is fed into a registry-side cache that the UI thread reads instantly. Subprocess timeout is also bounded at 1 second as defense in depth.handle_connect_resultdropped its own success result as "stale" if the scanner had already adopted the tunnel as Connected by the time the connect thread reported back (~1s race window). The post-connect bookkeeping —last_usedtimestamp, kill-switch sync,STATUS: Connectedlog line — was silently skipped, leaving the UI in a half-connecting state. Now acceptsConnected{this profile}as a non-stale arrival.
- TUI stays responsive on broken VPN servers. When a tunnel comes up but its server's routing is misconfigured (everything behind the VPN times out), the scanner and network-monitor threads were both probing the kernel's default-route every 1-2 seconds and each hitting their 1s timeout — burning two tokio runtime workers continuously and starving the scanner of cycles to do useful session work. The probe now shares a process-wide failure backoff: first 1-2 failures retry immediately, 3-5 cool down 5s, 6-10 cool down 15s, 10+ cap at 60s. Reset on any successful probe.
- Aggressive scroll-spam in the
vconfig viewer no longer wedges the TUI. Two compounding causes: (1) every keystroke rancontent.lines().count()for scroll bounds AND a full per-line re-parse + re-highlight for the render. On a multi-thousand-line.ovpn(typical when certs/keys are inlined), that was ~4N string-iterations per arrow-key. (2) Mouse wheels emit 30+ events per second; the event loop processed one event per render, so a fast scroll burst queued hundreds of events and the TUI ground through them long after the user stopped scrolling. Fix: aCachedConfigViewis built once when the viewer opens (pre-counted line total + pre-highlightedVec<Line>); the main event loop now drains every queued event into state before rendering, so a 100-event scroll burst lands in one render frame at the final position. - Connection-Details "Role" now correctly shows
Primaryfor OpenVPN profiles whose server pushesredirect-gatewayat runtime. The previous logic only inspected the client.ovpn(which has noredirect-gatewaydirective — the server pushes it viaPUSH_REPLYafter handshake), so every OpenVPN connect rendered asSplit tunnelregardless of whether the tunnel actually owned the kernel default route. Now the kernel routing table is the source of truth: any profile whose interface owns the default route renders asPrimaryregardless of what its static config declared. - New observability: any
Messagehandler that holds the UI thread for more than 50ms emits atracing::warn(silent by default; turn on withRUST_LOG=vortix::app=warn). Future regressions of this class surface immediately instead of being chased down with ad-hoc instrumentation. - CLI → TUI handoff for OpenVPN tunnels (#191). When a profile was connected via
vortix upand the TUI then opened, the sidebar sigil flashed grey (external— i.e. "not started by vortix") because the scanner couldn't authoritatively resolve the kernel interface from the runningopenvpnprocess. The scanner now reads the per-profile log file as Method 0 (platform-neutral, runs ahead of the lsof/ifconfig methods) and extracts the kernel interface via the existingparse_kernel_interfaceparser. Result: vortix-started tunnels render with the correct vortix-owned sigil regardless of which entry point launched them. - Manage-credentials save-only path no longer writes a plaintext-OTP
.scrv1.authbundle to disk. The deadstatic_challenge_promptfield is now explicitly cleared in the ManageAuth handler so the save path takes the username/password-only branch even when the profile declares a static-challenge directive. - TUI rendering no longer scrambled by config-ownership notes (#222).
fix_ownership()was writingNote: could not set ownership of …directly to stderr viaeprintln!, which corrupted ratatui's alternate-screen rendering when running as direct root (no sudo). Now routes throughtracing::debug!(SUDO_UID-unset case — chown is structurally impossible, no operator action needed) ortracing::warn!(real chown failures), both via the existing tracing infrastructure.
Removed
- The Security Guard panel no longer renders the sigil legend inline (moved to the
?help overlay) or theReal IP: <ip> (hidden)sub-bullet (avoids leaking the real IP in screenshots of an otherwise-clean panel). - Approach A dead code from the early static-challenge design (#191). An exploratory path tried to embed the SCRV1 envelope as a third line of the
--auth-user-passfile, but OpenVPN 2.7 does not consult that file for static-challenge responses. Removed: the SCRV1 branch informat_openvpn_auth_body, theotp: Option<&str>parameter onwrite_openvpn_auth_file, the dead branch inscrub_stale_scrv1_auth_files, and 5 dead tests covering that path. The shipping flow drives the management socket exclusively.
Install vortix 0.4.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/Harry-kp/vortix/releases/download/v0.4.0/vortix-installer.sh | shInstall prebuilt binaries via Homebrew
brew install Harry-kp/tap/vortixInstall prebuilt binaries into your npm project
npm install @harry-kp/vortix@0.4.0Download vortix 0.4.0
| File | Platform | Checksum |
|---|---|---|
| vortix-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| vortix-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| vortix-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| vortix-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
| vortix-aarch64-unknown-linux-musl.tar.xz | ARM64 MUSL Linux | checksum |
| vortix-x86_64-unknown-linux-musl.tar.xz | x64 MUSL Linux | checksum |