Repository navigation
v0.24.0
What's Changed
Fixed
-
Switching the preferred secret backend drops the old backend's unlock state (W1) — rebuilding the secret-manager chain on a settings save cleared the per-connection credential cache but left Bitwarden's process-global unlock marks (the session key, the verify timestamp, the last-sync timestamp) and the global-secret cache in place. After switching the preferred backend away from Bitwarden, a stale session key kept answering the fast path and a global secret resolved from the old backend could be served for up to its cache TTL. The switch now clears the Bitwarden session state and the global-secret cache — but only when the preferred backend actually changes, so flipping an unrelated read-only or root-search toggle still keeps an unlocked session (no re-lock, no speed regression). (
rustconn-core/src/secret/bitwarden.rs,rustconn/src/state/mod.rs) -
Tunnel askpass helper script is created atomically with 0700 (W4, review SEC-4) —
write_askpass_scriptcreated the helper withFile::createand only chmodded it to 0700 afterwards, so for a brief window it existed world-readable and a symlink planted at its (random) path could have redirected the write. It now opens the file withcreate_new(O_EXCL) and mode 0700 set at open time, matching the tunnel secret file. The script holds no secret (only the name of the env var to read); this is defence-in-depth. (rustconn-core/src/ssh_tunnel.rs) -
A mouse selection in a terminal no longer disappears while it is being dragged when the remote side redraws on a timer (contributed by Christian Petri, @Cyb0rk) — with byobu (or any tmux status bar with a clock) on the remote host, the selection was dropped every second, so selecting more than a few characters was nearly impossible. VTE deselects when output arrives before the drag ends, and protects its own PTY by not reading from it during a drag. RustConn owns the PTY and feeds VTE directly, which bypassed that pause. The output loop now waits while the primary button is down and VTE has a selection, and resumes on release or when the terminal loses focus. Nothing is dropped: the relay's bounded queue fills and the child is held back, as with VTE's own PTY. A click that goes to a mouse-tracking application (tmux with mouse on, vim) creates no selection and does not pause output. Closing the tab in the middle of a drag no longer leaves the output loop waiting for a release that never comes, and closing the tab or reconnecting in the middle of a drag no longer stalls the window while the session's PTY threads are stopped. (
rustconn/src/terminal/selection_hold.rs,rustconn/src/terminal/pty_relay.rs,rustconn/src/terminal/mod.rs) -
The current prompt line is no longer held back on prompts that use
❯and similar glyphs — on a host with an oh-my-bashpowerline-multilineprompt the last line (❯and the cursor) did not appear in a tab until more output arrived, although a separate terminal showed it at once. Since 0.23.6 the OSC 52 filter that sits in front of VTE treated a bare0x9dbyte as the 8-bit OSC introducer, but in UTF-8 that is a continuation byte inside ordinary characters (❯ise2 9d af, as are box-drawing characters such as╝and many Nerd Font glyphs), so the rest of the line was buffered as an OSC string; it happened with the OSC 52 setting off too. The filter now follows VTE, which decodes UTF-8 before parsing: the 8-bit controls are recognised only as the encoded code pointsc2 9dandc2 9c, andCAN/SUBand the other C1 controls abandon an open string. What VTE is fed is equivalent to the stream it would have read: the same text and controls, the same OSC strings, minus the OSC 52 offers the filter takes. Each sequence the filter removes (an offer, a query, an over-long string, or an OSC string left open when the session ends) is replaced by a singleCAN, which VTE ignores on screen and which ends whatever the filter let through just before it, so a loneESC, a UTF-8 lead byte or an abandoned OSC string cannot pair with the bytes that follow the gap, and a removed offer can no longer turn the text after it into an OSC 52 that VTE would act on. Offers VTE would also read as OSC 52 are no longer missed when control characters it ignores sit inside the string or betweenESCand], and an OSC string still open when the session ends is no longer handed to VTE, where the nextBELshown in the tab could complete it. Only a genuinely incomplete escape or a lonec2is held at the end of a read. (rustconn/src/terminal/osc52.rs) -
The browser SOCKS tunnel listens on loopback only, whatever
GatewayPortssays — the tunnel was started with-D <port>and no bind address, so aGatewayPorts yesin the user's~/.ssh/configmade the SOCKS proxy listen on every network interface and anyone on the local network could route traffic through the user's SSH or Tailscale host without authenticating. It is now-D 127.0.0.1:<port>for both OpenSSH and Tailscale SSH, the same address the readiness probe and the browsers already use. (rustconn-core/src/ssh_tunnel.rs) -
"Open Browser via Tunnel" ignores repeat clicks while its tunnel starts — the action raises the tunnel in the background (up to 10 seconds) with no sign that anything was happening, so a second click started a second tunnel and a second browser. It now shows a "Starting the tunnel…" toast on the click and ignores further clicks for the same connection until the tunnel is up or has failed. The in-flight mark is cleared on success, on failure and when the worker dies, and other connections are not affected. Quitting while the tunnel starts still leaves its
ssh -Nrunning (unchanged, deferred). (rustconn/src/window/edit_actions.rs) -
Jump-host tunnels for RDP, VNC and SPICE listen on loopback only (security) — the tunnel was started with
-L <port>:<host>:<port>and no bind address, soGatewayPorts yesin the user's~/.ssh/configmade the forwarded port listen on every network interface and anyone on the local network could reach the RDP/VNC/SPICE target through it. It is now-L 127.0.0.1:<port>:<host>:<port>for both OpenSSH and Tailscale SSH, built by the same helper as the SOCKS bind; every consumer already connects to127.0.0.1. Forwards the user writes themselves (per-connection port forwarding, the SSH Tunnel Manager) are unchanged on purpose and keep followingGatewayPorts. (rustconn-core/src/ssh_tunnel.rs,rustconn-core/src/models/protocol.rs,docs/USER_GUIDE.md) -
"Open Browser via Tunnel" is offered only where it can work (issue #397 follow-up) — the sidebar decided whether to show the item from a Zero Trust connection's
provider, while the code that starts the tunnel checked itsprovider_config, so an imported or hand-edited connection whose two disagreed showed the item and then did nothing. Both now use the same core predicate,rustconn_core::ssh_tunnel::supports_browser_tunnel, which requires the provider and its configuration to agree; the menu reads it from the row, and the Web connection "Tunnel Through SSH" path refuses a connection that cannot host a tunnel instead of runningsshagainst it. If the action is still reached for such a connection, it shows the "Could not open the SSH tunnel" toast. (rustconn-core/src/ssh_tunnel.rs,rustconn/src/sidebar/view.rs,rustconn/src/window/edit_actions.rs) -
The CPU/RAM monitoring bar no longer stays washed out after Reconnect (issue #398) — when an SSH session dropped, the monitoring collector stopped after three consecutive execution errors and dimmed the bar (the
monitoring-stalestyle, 50% opacity) with a warning icon to mark its metrics as stale. Reconnect then skipped starting monitoring, because its guard treated any bar already in the session map as live, so the dimmed bar stayed in place and its metrics never updated again — including the race where Reconnect was pressed before the collector had even noticed the drop. Reconnect now explicitly discards the previous connection's bar and collector before reconnecting, and starting monitoring replaces a bar whose collector has already stopped instead of skipping it, so the bar comes back exactly as on a first connect: one bar, no stale class, no warning icon. (rustconn/src/monitoring.rs,rustconn/src/window/protocols_ssh.rs) -
Embedded RDP hands a session that stalls after TLS to the external client (issue #392) — against GNOME Remote Desktop's Remote Login mode, IronRDP completed the TLS upgrade and the TOFU certificate check, then waited the full handshake budget (60s+) inside
connect_finalize(NLA → MCS → licensing → capabilities) with no error. That timeout was indistinguishable from an unreachable host (Operation timed out, aNON_FALLBACK_MARKERSentry), so the session closed with no fallback even though Remmina and GNOME Connections — both FreeRDP-based — connect to the same server. A timeout after the TLS upgrade has already succeeded now classifies asProtocolIncompatibleand hands the session to the externalxfreerdp3client, reusing the same fallback path issue #199 added; a timeout before TLS, a refused connection and an unknown host are unaffected and still fail without a fallback. The root cause is upstream: GNOME Remote Desktop's Remote Login mode most likely expects an RDP Server Redirection to one-time RDSTLS credentials, whichironrdp-connector0.10 does not implement (cluster: None, upstream TODO #139); full embedded support needs that upstream work. Until then, selecting Client mode: External for such a connection skips the ~minute wait. (rustconn-core/src/rdp_client/failure.rs,rustconn-core/src/rdp_client/client/connection.rs,docs/USER_GUIDE.md) -
F11 in a popped-out session window now gives a true, chrome-free full screen (issue #388) — pressing F11 on a detached window (right-click an embedded tab → "Pop Out") fullscreened the window but left its header bar ("Move to Main Window" button and title) on screen, unlike the main window, which hides its header bar in full screen. The detached window now follows its own
fullscreenedproperty, the same mechanism the main window's fullscreen chrome uses: the header bar hides on entry and is restored exactly on exit, while the "Session disconnected" banner stays visible (issue #236). This covers every path into full screen — F11, the window manager, and detaching straight to a monitor — and thewin.toggle-fullscreenaction's checkmark state now follows the same property instead of going stale after a window-manager-driven change. (rustconn/src/detached_window.rs,rustconn/src/window/detach_actions.rs)
Added
- Performance instrumentation for vault and SSH operations (
rustconn::perf, W0) — 0.24 is a performance release, so its targets are measured rather than felt. A newPerfSpanhelper times each vault operation (vault.unlock,vault.retrieve/store/delete) and each SSH-connect phase (ssh.connect.resolve,ssh.connect.agent,ssh.connect.args) and, on drop, emits one structured line to therustconn::perftracing target withop,transport,elapsed_msandcallsfields. The target is off unless you ask for it (RUST_LOG=rustconn::perf=info), and a line never carries a secret, a credential, a vault item name, a host name or a user name — only fixed labels. This is the instrument the release's timing targets are read from; see docs/ARCHITECTURE.md. (rustconn-core/src/tracing/perf.rs,rustconn-core/src/secret/bitwarden.rs,rustconn/src/vault_ops.rs,rustconn/src/state/mod.rs,rustconn/src/window/protocols_ssh.rs) - Bitwarden
bwspawn-count and argv-safety harness (W0, dev test) — an ignored integration test puts a fakebwon the backend's resolved command path and runs realBitwardenBackendoperations against it, so the number of child processes per vault operation can be measured in CI as the W2bw servework drives it toward one. It also pins the 0.23.21 fix that the item JSON (which carries the plaintext password) reachesbwover stdin and never as a base64 argument visible in/proc/<pid>/cmdline. Run it withcargo test -p rustconn-core --test bitwarden_spawn_counter -- --ignored. (rustconn-core/tests/bitwarden_spawn_counter.rs) - Containerized sshd testbed for live SSH integration tests (W3 K0) — a throwaway rootless-podman OpenSSH server (
scripts/sshd-testbed.sh) with named profiles (password, bastion, StrictModes, publickey+keyboard-interactive, ForceCommand, unknown host key), bound to a random loopback port and torn down with the container, so the SSH primitives that need a real server answering on a real socket can be exercised without touching the developer's own sshd or~/.ssh. A new ignored test module drives it against the pre-connect port check (check_port/check_port_async), the ControlMaster socket detection behind the P4 fast path (control_socket_present) and the interactive keepalive, and self-skips cleanly when no testbed env is present. No new dependency and no production code — pure shell plus#[ignore]tests. Run withscripts/sshd-testbed.sh up <profile>,source target/sshd-testbed.env, thencargo test -p rustconn-core --test ssh_testbed -- --ignored. (scripts/sshd-testbed.sh,rustconn-core/tests/ssh_testbed.rs) - Headless core of "Set Up Key Authentication…" (W3 K2) — a new
rustconn_core::ssh_key_setupmodule does everything the "Set Up Key Authentication…" dialog (next entry) needs without a widget: it generates a key withssh-keygen(Ed25519 by default, RSA 4096 as an option, no passphrase by default) into<ssh dir>/rustconn/(mode 0700,~/snap/rustconn/current/.ssh/rustconn/in the snap) under<connection>-<id8>_<type>with the commentrustconn@<host> <date>and no connection name; installs the public key with its own idempotent script fed through stdin (creates~/.ssh0700 andauthorized_keys0600, appends only when the key is absent, runsrestorecon) so a hostile connection name, host or comment has nothing to inject into; and verifies the result by logging in with the new key alone (no ControlMaster, no agent,publickeyonly) and counting it a success only when the server's-voutput names the new key's fingerprint. The control-channelsshalways runs withStrictHostKeyChecking=yes, placed ahead of — and removing the conflicting entries from — the connection's custom options, soStrictHostKeyChecking=nothere cannot weaken it; an unknown or changed host key is reported as its own error so the dialog can offer to connect normally first. A password goes tosshthrough the askpass helper (a file path in the environment, never argv); a key passphrase is used only to generate or verify and is never stored, and during verification it is released only for the passphrase prompt of the new key, so a bastion's password prompt in a jump chain never receives it. Clustered short flags in a connection's ownssharguments (-vA,-vo …) are split before filtering, so they cannot re-enable agent forwarding or a weaker host-key setting, and a host name with shell metacharacters is refused. A repeat run reuses the pair it generated before, and a generated key can be deleted without ever touching a key the user brought.proc::wait_bounded_cancellablelets a Cancel button stop the child. Nothing in the UI changes yet. (rustconn-core/src/ssh_key_setup.rs,rustconn-core/src/proc.rs,rustconn-core/src/ssh_tunnel.rs,rustconn-core/tests/ssh_testbed.rs) - "Set Up Key Authentication…" in the sidebar context menu (W3 K3) — right-click an SSH connection in the sidebar and choose "Set Up Key Authentication…" to stop typing a password for it. RustConn creates a key (Ed25519 by default, RSA 4096 as an option), installs it on the server, signs in with it to check that it works, and then switches the connection to key authentication, with an Undo button on the confirmation toast. The sidebar menu is the only entry point for now; a button in the connection editor comes later. It reaches the server exactly the way a normal session does, including jump hosts,
ProxyCommandand the Flatpakknown_hosts, and it hands the stored account password tosshonly when a normal connect would. The stored password stays in your password store, so you can switch back. If the connection has no stored password, the dialog asks for it once and does not save it. An optional passphrase protects the new key; it is used only to create and check that key and is never stored. Nothing is sent to a server whose host key RustConn has not seen: the dialog says so and offers "Connect" to open an ordinary session so the key gets recorded (when a stored password signs that session in, the key is recorded without a question, as in any normal connect), after which you run the setup again; a changed host key only shows a warning. If the server rejects a stored password, the dialog shows the password field so you can type the right one for this run. Other outcomes have their own messages: wrong or expired password, wrong passphrase, a server that does not allow password sign-in, a server with no POSIX shell, the key being installed but refused by the server (StrictModes, a customAuthorizedKeysFile, SELinux), and network errors. If the key is installed but the check fails, it is kept so that "Try Again" reuses it, and "Delete Generated Key" removes it. A server that accepts the key but also asks for a one-time code lets you decide whether to use the key anyway. Stop cancels a run in progress and ends thesshprocess. In the snap, the dialog warns that the key lives in the snap's own folder. This also tightens the K2 key check: behind a jump host, the bastion's own debug output no longer turns a wrong passphrase into "the server does not accept the key" or its partial success into a second-factor warning. (rustconn/src/dialogs/key_setup.rs,rustconn/src/window/key_setup.rs,rustconn/src/window/protocols_ssh.rs,rustconn/src/window/edit_actions.rs,rustconn/src/sidebar_ui.rs,rustconn/src/sidebar/view.rs,rustconn/src/state/mod.rs,rustconn-core/src/ssh_key_setup.rs,po/*.po) - "Use an Existing Key" in Set Up Key Authentication (W3 K5) — the "Set Up Key Authentication…" dialog (K3) gained a "Key" row: "Create a New Key" (unchanged) or "Use an Existing Key". The existing-key list is read from
~/.sshand~/.ssh/rustconn(the same directory generated keys live in), with a "Choose file…" button that opens the portal file chooser for a key kept elsewhere; in Flatpak a portal path is copied to the stable sandbox.sshdirectory first, as the connection editor's own key picker already does. A connection that already points at a key file has it preselected. The public half is read from a matching.pubfile when one parses and agrees with the private key's fingerprint, otherwise derived straight from the OpenSSH private key itself — which works even for an encrypted key, since OpenSSH keeps the public key in the clear; a legacy PEM/PKCS#8 key (not parsed by this module) needs a.pubfile next to it. Only Ed25519 and RSA are supported, matching what key setup can generate. Deploy, verify and the outcome mapping (including "Connect" for an unknown host key) are exactly K3's, run against the chosen key instead of a generated one; on success the connection switches to that key's path with an Undo toast, and the stored account password is untouched. If the chosen key is encrypted, its passphrase is asked for in the dialog and used only to verify the key signs in — never saved, never vaulted. The one rule this adds: an existing key the user brought is never offered for deletion on a failed run, unlike a key RustConn generated —KeyOrigintracks where each on-disk key came from, anddelete_offered/deletable_keyare the only two places "Delete Generated Key" or the deletion itself may fire from. (rustconn/src/dialogs/key_setup.rs,rustconn/src/window/key_setup.rs,rustconn-core/src/ssh_key_setup.rs,rustconn-core/src/ssh_agent/keyfile.rs,rustconn-core/src/ssh_agent/mod.rs,po/*.po) - "Set Up Key Authentication…" in the connection editor (W3 K4) — the Authentication group on an SSH connection's page now has a "Set Up Key Authentication…" row, the same item and the same dialog as the sidebar menu's (K3/K5): the sidebar menu is no longer the only entry point. The row is enabled only for an already-saved SSH connection (disabled with a "Available after the connection is saved" hint otherwise, and absent for SFTP/MOSH/every other protocol, which cannot host the flow). It runs against the connection as it is currently stored, so an unsaved edit to Host is not silently swapped in. Unlike the sidebar menu, a result from the editor's row is never persisted directly: on success the dialog closes and the key goes into the editor's own Method, Key Source, Key File and "Use Only Specified Key" fields, exactly what the sidebar menu would have written, and it only takes effect when you press Save — Cancel discards it like any other unsaved edit (the key itself stays installed on the server and on disk either way). No Undo toast in this path, since the editor's own Cancel already fills that role. (
rustconn/src/dialogs/connection/ssh.rs,rustconn/src/dialogs/connection/dialog/mod.rs,rustconn/src/dialogs/connection/dialog/build.rs,rustconn/src/dialogs/connection/dialog/construction.rs,rustconn/src/window/key_setup.rs,rustconn/src/window/edit_actions.rs,rustconn/src/window/edit_dialogs.rs,rustconn/src/window/copy_field_actions.rs,rustconn/src/window/mod.rs,po/*.po) - In-process SSH key-file inspection and fingerprinting (W3 A2) — a new
ssh_agent::keyfilemodule reads the public half of a private OpenSSH key, computes itsSHA256:fingerprint and classifies the file (OpenSSH vs legacy PEM vs PKCS#8, encrypted vs not) without spawningssh-keygenand without a passphrase — the public key sits in the clear even in an encrypted OpenSSH key, so a key can be identified before anyone is asked to unlock it. The fingerprint is computed with the samering+data-encodinghelper the agent read path uses, so a file key and an agent key compare directly (the basis for an "is this key already loaded?" check). It adds no dependency: the full A2 plan to decrypt in-process via thessh-keycrate is blocked by a transitiveaes-gcmRC-pin conflict with IronRDP, so an encrypted key still routes tossh-addfor loading (KeyFileInfo::needs_ssh_add_fallback). (rustconn-core/src/ssh_agent/keyfile.rs,rustconn-core/src/ssh_agent/wire.rs,rustconn-core/src/ssh_agent/mod.rs) - Embedded web browser in the snap (issue #244) — the snap now builds the default feature set plus
adw-1-8, so Web connections can open in an embedded WebKitGTK 6.0 tab, using the WebKitGTK 2.52 of the gnome-core26 platform and thewebkitgtk-6.0layout the core26gnomeextension adds. Inside a snap WebKit does not use its bubblewrap sandbox, so embedded web content is confined by the snap's strict confinement only and has the same access as RustConn itself (home, SSH keys, cloud credentials, keyring); docs/SNAP.md describes the trade-off. The embedded browser is the default in the snap, the same as in the Flatpak and the native packages: new Web connections, imports (mRemoteNG, CSV) and the connection dialog's Mode list default to Embedded, and Open Browser via Tunnel defaults to the in-app browser. System and Custom remain available in the Mode list for a connection where the missing sandbox matters, and the tunnelled-browser switch still lets you pick an external Chromium-based browser instead; docs/SNAP.md spells out when to reach for either. Open Browser via Tunnel with no usable Chromium opens the embedded browser instead of the system one (which cannot take the SOCKS proxy), with a toast explaining the fallback; a Web connection tunnelled through SSH needs Embedded or a Chromium Custom command, since System opens it without the tunnel. Flatpak, native and macOS behaviour is unchanged. The feature list no longer repeatsdefaultby hand, so a new default feature cannot silently go missing from the snap. (snap/snapcraft.yaml,docs/SNAP.md,docs/USER_GUIDE.md) - "Open Browser via Tunnel" for Tailscale SSH connections (issue #397) — the sidebar action that raises a SOCKS proxy to a host and opens a browser through it was offered only for SSH/SFTP connections, so a Tailscale SSH user had to keep a duplicate plain-SSH connection just to browse through a tailnet host. A Zero Trust connection of the Tailscale SSH type now offers it too. The proxy is started with the same
tailscale ssh [user@]hostthe session runs, with-N -D <port>passed through to the systemssh, so authentication, ACLs, MagicDNS and Tailscale's host-key check are exactly the session's; the connection's port, custom arguments and stored password are not used, as the session does not use them either. A host or user name that is not a plain name (letters, digits,.,_,-,:) is refused before anything is spawned, sincetailscale sshpasses it tosshwhere a leading-would read as an option. Embedded and external (Chromium) browsers both work, as for SSH. Other Zero Trust providers are unchanged. One shared gate,rustconn_core::ssh_tunnel::socks_tunnel_route, now decides which connections can host the tunnel, and the tunnel builder gained aTunnelLauncherinstead of a second copy. (rustconn-core/src/ssh_tunnel.rs,rustconn-core/src/models/protocol.rs,rustconn/src/window/edit_actions.rs,rustconn/src/sidebar_ui.rs,rustconn/src/sidebar/view.rs,rustconn/src/window/types.rs,docs/USER_GUIDE.md)
Changed
-
Listing ssh-agent identities no longer spawns a process (W1 A1) — reading the keys an agent holds ran
ssh-add -land, for the public-key path, assh-keygenper key, so a connect that consulted the agent cost one process plus one per identity on the way in. RustConn now speaks the ssh-agent protocol directly overSSH_AUTH_SOCK(SSH_AGENTC_REQUEST_IDENTITIES) in one round trip and computes each key's SHA256 fingerprint in-process withring— no process, under a millisecond. The oldssh-add -lpath remains as a fallback for an agent the native client cannot talk to, so behaviour is never worse than before. Adding and removing keys still go throughssh-addfor now. (rustconn-core/src/ssh_agent/wire.rs,rustconn-core/src/ssh_agent/mod.rs) -
One
ssh-addpath for the whole app (W1 A3) — loading a connection's SSH key into the agent before an mc/file-manager launch was five near-identicalssh-addspawns (four in the GUI, one in core), each re-deriving the same stdin/stderr wiring and FlatpakSSH_ASKPASSstrip, so a fix to one could miss the others. They now call a singlerustconn_core::sftp::ensure_identity_loaded, which returns the agent's stderr on failure so each caller keeps only its own result handling (a toast, a sidebar status, a log line). No behaviour change: a missing or absent key still proceeds without blocking the launch. (rustconn-core/src/sftp.rs,rustconn/src/window/edit_actions.rs) -
Secret global variables are read from the vault once per session window, not once per connect phase (W1 P1) — a connection that substitutes a secret global variable re-read it from the vault every time, and a single SSH connect reads the globals two to three times (the ASK scan, the launch substitution, the reconnect path), each a blocking vault lookup. Resolved secret globals are now held in a 300-second session cache (the same window as the per-connection credential cache), so the repeated reads of one connect and a quick reconnect cost nothing; a vault miss is not cached, and saving the variables clears the cache so an edited secret is never served stale. (
rustconn/src/vault_ops.rs,rustconn/src/window/history_actions.rs) -
ssh -Gno longer spawns on the GTK main thread during SSH connect (W1 P3) — deciding whether OpenSSH's own config routes a host through a proxy (which gates whether the target password may be delivered by askpass) ranssh -Gsynchronously inside the connect-args build on the UI thread. On the initial connect it now runs in the same background phase as the port check and the answer is handed to the build, so the main thread spawns no process. The reconnect path and a connect with the port check disabled still compute it synchronously as a fallback. No behaviour change: a config that routes through a proxy still blocks target-password askpass, keeping the credential off the bastion. (rustconn/src/window/protocols_ssh.rs) -
FreeRDP version detection is warmed in the background at startup (W4) — the first RDP connect of a session probed each candidate FreeRDP client (
… --version) synchronously on the GTK main thread to pick and configure the right binary — up to seven short spawns the user waited through while the window was already up (one connect in the field showed five). A fire-and-forget background thread at startup now fills the same process-wide version cache the connect path reads (by the same keys, localPATHand the Flatpak host'shost:form), so by the time a connection opens the lookups are cache hits and the main thread spawns nothing. It changes no detection logic and caches nothing a probe would not: a client missing, slow or without a parseable banner at warm-up time is simply re-probed on first use, so behaviour is never worse than before. (rustconn/src/embedded_rdp/detect.rs,rustconn/src/app.rs) -
Loading an SSH key into the agent skips
ssh-addwhen the key is already there (W3 A3) — the singleensure_identity_loadedpath added in W1 always spawnedssh-add, even before an mc or SFTP launch on a key already loaded at connect time, where the add is a no-op that still costs a process. It now reads the key's public fingerprint without a passphrase (A2, the public half of an OpenSSH key is in the clear even when encrypted; a sibling.pubis the fallback) and compares it against the agent's identities over the native wire protocol (A1, one Unix-socket round trip, no process). An already-loaded key returns immediately with zero spawns. The agent query is bounded by a 3-second timeout so a broken or launchd-throttled agent cannot hang a main-thread caller, and any uncertainty (no fingerprint, no agent, a timeout) falls through to the existingssh-addpath — so behaviour is never worse than before and an encrypted key not yet loaded still goes throughssh-add. (rustconn-core/src/sftp.rs) -
A live ControlMaster socket skips the pre-connect port check (W1 P4) — when a master connection to the host is already open, a new SSH session multiplexes over it, so the TCP connection is already up and the pre-connect port check is redundant. RustConn now detects an existing ControlMaster socket for the host and port (a real Unix socket sharing the
ControlPathprefix, not a stale regular file) and skips the port check, saving a round trip. It does not runssh -O check— that would spawn the process the fast path exists to avoid — and a socket that is present but dead is simply re-established by OpenSSH'sControlMaster=auto, so the only cost of a rare false positive is a skipped port check that proxy/bypass connections already skip safely. (rustconn-core/src/monitoring/ssh_exec.rs,rustconn/src/window/protocols_ssh.rs) -
Multi-bastion hop-password resolution extracted into one tested function (W1 P2a) — the per-hop password logic for a jump-host chain (issue #203) was inline in the connect-args build, interleaved with the chain walk. It now lives in
assemble_hop_passwords, which keeps the first reference hop's already-resolved password in place, resolves each deeper hop through an injected resolver (cache then vault, warming the cache), and keeps the result index-aligned with the jump-host list so a string-proxy gap or an unresolvable hop never shifts the others. The vault read is still synchronous — this is the extraction and dedup step; moving it off the main thread is a separate change. (rustconn/src/window/protocols_ssh.rs) -
Bitwarden unlock no longer blocks on
bw sync(W1) — unlocking the vault ranbw syncsynchronously before handing back a ready backend, and on a snap/Flatpakbwthat one call cost around six seconds on the critical path to a usable vault at startup (issue #353 trace:bw status+bw unlock+bw syncback to back, ~15 s total). The sync is now deferred rather than removed: the unlock path returns immediately, the startup thread runs the sync in the background after signalling the UI that the vault is unlocked, and the first credential read or write after an unlock carries a one-shotsync_due()gate so the local copy is still refreshed before it is used (freshness for items edited in another client, issue #351, is preserved). A sync that ran within the last ten minutes is not repeated, so reconnects and batch lookups keep skipping the round trip. The background and first-retrieve syncs are serialized process-wide, so a connect made the instant the background sync starts waits for it rather than firing a secondbw syncbeside it. (rustconn-core/src/secret/bitwarden.rs,rustconn/src/app.rs) -
SSH askpass helper moved from the GUI into
rustconn-core(W3 K1) — the prompt-awareSSH_ASKPASShelper that releases a stored target or bastion password only for an account-password prompt (and stays silent for key passphrases, host-key questions, OTP and password-change prompts), its per-hop script cache, the owner-only secret files and the per-child askpass environment lived inrustconn/src/window/protocols_ssh.rs, out of reach of the core consumers that need them next (key setup, the CLI, monitoring). They now live in a newrustconn_core::ssh_askpassmodule that the GUI calls, and the SSH tunnel's secret file is written by the same function, so one place decides where an askpass secret may live ($XDG_RUNTIME_DIR,$TMPDIRon macOS, an O_EXCL 0600 random name in the temp dir otherwise). No behaviour change: the helper script is byte-for-byte the same (pinned by a golden test), as are the env var names, file names, modes and the cleanup on ssh exit. (rustconn-core/src/ssh_askpass.rs,rustconn-core/src/ssh_tunnel.rs,rustconn/src/window/protocols_ssh.rs)
Removed
- Dead embedded-RDP file-transfer progress/complete callbacks (W4, issue #390) —
connect_file_progressandconnect_file_complete, their backingon_file_progress/on_file_completefields and the whole chain that carried them (theFileTransferContextfields, the clones through the connect setup and the Save-Files button, and the three call sites that invoked them) had no caller: nothing ever connected a callback, so every invocation sawNoneand the "Downloaded {}/{} files" / completion branches were dead. The user-facing progress and summary come from the status label and the button, which are untouched; this only removes machinery that never ran. (rustconn/src/embedded_rdp/mod.rs,rustconn/src/embedded_rdp/connection.rs,rustconn/src/embedded_rdp/polling_handlers.rs,rustconn/src/embedded_rdp/thread.rs) - Dead
RdpBackendSelectorbackend-selection module (W4, FC §6c) —rdp_client/backend.rs(theRdpBackendenum,BackendDetectionResultandRdpBackendSelectorwith its detection caching) was a relic of an older backend-selection design and had no production caller: the live FreeRDP-client choice goes throughembedded_rdp::detectandprotocol::select_freerdp_client. Removed the module, its re-exports and its property tests (~380 lines with tests). No behaviour change — nothing outside the module and its own tests referenced it. (rustconn-core/src/rdp_client/backend.rs,rustconn-core/src/rdp_client/mod.rs,rustconn-core/tests/properties/rdp_backend_tests.rs)
Improved
- "Open Browser via Tunnel" no longer freezes the window while the SSH tunnel comes up (W4) — the sidebar action started the SOCKS tunnel and waited for its port on the GTK thread, up to 10 seconds with a dead or slow host, while the Web connection
tunnel_viapath already did the same off-thread. The action now raises the tunnel through the same background path, so the window stays responsive; the success and failure toasts, the choice between the embedded and the external browser, and who owns the tunnel afterwards (the tab for the embedded browser, the external-session registry for Chromium) are unchanged. (rustconn/src/window/edit_actions.rs)
Dependencies
- Snap base core24 → core26, GNOME platform
gnome-46-2404→gnome-core26(issue #174) — Snapcraft 9.2 added core26 support to thegnomeextension, which was the blocker that reverted the 0.23.2 attempt. The snap now builds on Ubuntu 26.04 against thegnome-core26platform (GTK 4.24, libadwaita 1.10) with GPU drivers frommesa-2604, and is compiled withadw-1-8like the Flatpak, so it gets the full libadwaita 1.8 widget tier (AdwSpinner,AdwToggleGroup,AdwShortcutsDialog) instead of the 1.5 fallbacks. Existing snap users download the newgnome-core26andmesa-2604platform snaps on the first update. Because core26 ships no Python, the pip-installed Azure and Oracle Zero Trust CLIs in the Components dialog run on the Python 3.14 of the gnome-core26 platform (the extension's library cleanup empties a separately staged Python), and because the extension no longer adds thegsettingsplug the snap declares it explicitly. The bundled FreeRDP client staysfreerdp3-x11; the embedded WebKit browser is enabled separately (see Added, issue #244). Building the snap needs Snapcraft 9.2 or newer. (snap/snapcraft.yaml,.github/workflows/snap.yml,.github/workflows/release.yml,scripts/check-snap-runtime-libs.sh) - crypto-bigint 0.7.6 (yanked) → 0.7.10 — a
cargo updatecleared a yanked-crate warning (cargo deny check advisoriesflagged crypto-bigint 0.7.6, pulled transitively through the IronRDP → picky → rsa-rc → crypto-primes chain); advisories are otherwise clean before and after. - Updated (semver-compatible): ctutils 0.4.5 → 0.4.6, der 0.8.2 → 0.8.3, elliptic-curve 0.14.1 → 0.14.3, ironrdp-error 0.2.0 → 0.2.1, lzma-rust2 0.22.0 → 0.23.0, notify 8.2.0 → 8.2.1, notify-types 2.1.0 → 2.2.0, openssl-probe 0.2.1 → 0.2.2, picky-asn1 0.10.1 → 0.10.2, picky-asn1-x509 0.15.5 → 0.15.6, roxmltree 0.21.1 → 0.21.2, rustix 1.1.5 → 1.1.6, typed-path 0.12.4 → 0.13.0, xattr 1.6.1 → 1.7.0, zip 9.0.5 → 9.1.3. The update also pruned ten stale
windows-*lock entries;cargo-sources.jsonregenerated for the Flatpak/Flathub manifests. The Flatpak stack was already current: GNOME runtime 51, FreeRDP 3.32.2 (latest upstream), cJSON 1.7.19, OpenH264 2.7.0, VTE 0.84.1; every downloadable CLI tool resolves current (kubectl 1.37.2, Tailscale 1.104.4, Teleport 18.10.0).
Installation
Flatpak (Recommended)
flatpak install flathub io.github.totoshko88.RustConnSnap
sudo snap install rustconnDebian/Ubuntu (.deb from this release)
sudo dpkg -i rustconn_0.24.0_amd64.deb
sudo apt-get install -f # Install dependencies if neededFedora (.rpm from this release)
sudo dnf install rustconn-0.24.0-1.fc44.x86_64.rpmAppImage
chmod +x RustConn-0.24.0-x86_64.AppImage
./RustConn-0.24.0-x86_64.AppImagemacOS (Homebrew)
brew tap totoshko88/rustconn
brew install rustconn
open $(brew --prefix)/opt/rustconn/RustConn.appAll dependencies (GTK4, libadwaita, VTE, Adwaita icons) are installed automatically.
Requires macOS 13 (Ventura) or later.
OBS Repositories
Packages available at: https://build.opensuse.org/package/show/home:totoshko88:rustconn/rustconn
# Debian 13 (Trixie)
echo 'deb http://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Debian_13/ /' \
| sudo tee /etc/apt/sources.list.d/rustconn.list
curl -fsSL https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Debian_13/Release.key \
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rustconn.gpg > /dev/null
sudo apt update && sudo apt install rustconn
# Ubuntu 24.04 LTS (Noble)
echo 'deb http://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_24.04/ /' \
| sudo tee /etc/apt/sources.list.d/rustconn.list
curl -fsSL https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_24.04/Release.key \
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rustconn.gpg > /dev/null
sudo apt update && sudo apt install rustconn
# Ubuntu 26.04 LTS (Resolute)
echo 'deb http://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_26.04/ /' \
| sudo tee /etc/apt/sources.list.d/rustconn.list
curl -fsSL https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/xUbuntu_26.04/Release.key \
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rustconn.gpg > /dev/null
sudo apt update && sudo apt install rustconn
# Fedora 44
sudo dnf config-manager addrepo --from-repofile=https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Fedora_44/home:totoshko88:rustconn.repo
sudo dnf install rustconn
# Fedora 43
sudo dnf config-manager addrepo --from-repofile=https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/Fedora_43/home:totoshko88:rustconn.repo
sudo dnf install rustconn
# openSUSE Tumbleweed
sudo zypper ar https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/openSUSE_Tumbleweed/ rustconn
sudo zypper ref && sudo zypper in rustconn
# openSUSE Leap 16.0
sudo zypper ar https://download.opensuse.org/repositories/home:/totoshko88:/rustconn/openSUSE_Leap_16.0/ rustconn
sudo zypper ref && sudo zypper in rustconnArch Linux (AUR)
yay -S rustconnFreeBSD (Ports)
pkg install rustconnFull installation guide: https://github.com/totoshko88/RustConn/blob/main/docs/INSTALL.md
Verifying your download
Every binary on this release (deb, rpm, AppImage, Flatpak) is covered two ways.
Plain checksums — download SHA256SUMS alongside the asset, then:
sha256sum -c SHA256SUMSSigned build provenance (SLSA, via Sigstore) — proves the file was built by this
workflow from this commit:
gh attestation verify RustConn-0.24.0-x86_64.AppImage --repo totoshko88/RustConn(The snap is signed by the Snap Store; the macOS build is delivered via the Homebrew tap.)