Skip to content

v0.24.0

Choose a tag to compare

@github-actions github-actions released this 28 Oct 10:47
· 22 commits to main since this release

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_script created the helper with File::create and 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 with create_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-bash powerline-multiline prompt 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 bare 0x9d byte as the 8-bit OSC introducer, but in UTF-8 that is a continuation byte inside ordinary characters (❯ is e2 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 points c2 9d and c2 9c, and CAN/SUB and 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 single CAN, which VTE ignores on screen and which ends whatever the filter let through just before it, so a lone ESC, 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 between ESC and ], and an OSC string still open when the session ends is no longer handed to VTE, where the next BEL shown in the tab could complete it. Only a genuinely incomplete escape or a lone c2 is held at the end of a read. (rustconn/src/terminal/osc52.rs)

  • The browser SOCKS tunnel listens on loopback only, whatever GatewayPorts says — the tunnel was started with -D <port> and no bind address, so a GatewayPorts yes in the user's ~/.ssh/config made 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 -N running (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, so GatewayPorts yes in the user's ~/.ssh/config made 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 to 127.0.0.1. Forwards the user writes themselves (per-connection port forwarding, the SSH Tunnel Manager) are unchanged on purpose and keep following GatewayPorts. (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 its provider_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 running ssh against 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-stale style, 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, a NON_FALLBACK_MARKERS entry), 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 as ProtocolIncompatible and hands the session to the external xfreerdp3 client, 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, which ironrdp-connector 0.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 fullscreened property, 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 the win.toggle-fullscreen action'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 new PerfSpan helper 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 the rustconn::perf tracing target with op, transport, elapsed_ms and calls fields. 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 bw spawn-count and argv-safety harness (W0, dev test) — an ignored integration test puts a fake bw on the backend's resolved command path and runs real BitwardenBackend operations against it, so the number of child processes per vault operation can be measured in CI as the W2 bw serve work drives it toward one. It also pins the 0.23.21 fix that the item JSON (which carries the plaintext password) reaches bw over stdin and never as a base64 argument visible in /proc/<pid>/cmdline. Run it with cargo 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 with scripts/sshd-testbed.sh up <profile>, source target/sshd-testbed.env, then cargo 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_setup module does everything the "Set Up Key Authentication…" dialog (next entry) needs without a widget: it generates a key with ssh-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 comment rustconn@<host> <date> and no connection name; installs the public key with its own idempotent script fed through stdin (creates ~/.ssh 0700 and authorized_keys 0600, appends only when the key is absent, runs restorecon) 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, publickey only) and counting it a success only when the server's -v output names the new key's fingerprint. The control-channel ssh always runs with StrictHostKeyChecking=yes, placed ahead of — and removing the conflicting entries from — the connection's custom options, so StrictHostKeyChecking=no there 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 to ssh through 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 own ssh arguments (-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_cancellable lets 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, ProxyCommand and the Flatpak known_hosts, and it hands the stored account password to ssh only 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 custom AuthorizedKeysFile, 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 the ssh process. 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 ~/.ssh and ~/.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 .ssh directory 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 .pub file 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 .pub file 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 — KeyOrigin tracks where each on-disk key came from, and delete_offered/deletable_key are 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::keyfile module reads the public half of a private OpenSSH key, computes its SHA256: fingerprint and classifies the file (OpenSSH vs legacy PEM vs PKCS#8, encrypted vs not) without spawning ssh-keygen and 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 same ring + data-encoding helper 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 the ssh-key crate is blocked by a transitive aes-gcm RC-pin conflict with IronRDP, so an encrypted key still routes to ssh-add for 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 the webkitgtk-6.0 layout the core26 gnome extension 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 repeats default by 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@]host the session runs, with -N -D <port> passed through to the system ssh, 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, since tailscale ssh passes it to ssh where 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 a TunnelLauncher instead 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 -l and, for the public-key path, a ssh-keygen per 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 over SSH_AUTH_SOCK (SSH_AGENTC_REQUEST_IDENTITIES) in one round trip and computes each key's SHA256 fingerprint in-process with ring — no process, under a millisecond. The old ssh-add -l path 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 through ssh-add for now. (rustconn-core/src/ssh_agent/wire.rs, rustconn-core/src/ssh_agent/mod.rs)

  • One ssh-add path for the whole app (W1 A3) — loading a connection's SSH key into the agent before an mc/file-manager launch was five near-identical ssh-add spawns (four in the GUI, one in core), each re-deriving the same stdin/stderr wiring and Flatpak SSH_ASKPASS strip, so a fix to one could miss the others. They now call a single rustconn_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 -G no 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) ran ssh -G synchronously 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, local PATH and the Flatpak host's host: 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-add when the key is already there (W3 A3) — the single ensure_identity_loaded path added in W1 always spawned ssh-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 .pub is 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 existing ssh-add path — so behaviour is never worse than before and an encrypted key not yet loaded still goes through ssh-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 ControlPath prefix, not a stale regular file) and skips the port check, saving a round trip. It does not run ssh -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's ControlMaster=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 ran bw sync synchronously before handing back a ready backend, and on a snap/Flatpak bw that one call cost around six seconds on the critical path to a usable vault at startup (issue #353 trace: bw status + bw unlock + bw sync back 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-shot sync_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 second bw sync beside 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-aware SSH_ASKPASS helper 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 in rustconn/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 new rustconn_core::ssh_askpass module 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, $TMPDIR on 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_progress and connect_file_complete, their backing on_file_progress/on_file_complete fields and the whole chain that carried them (the FileTransferContext fields, 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 saw None and 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 RdpBackendSelector backend-selection module (W4, FC §6c) — rdp_client/backend.rs (the RdpBackend enum, BackendDetectionResult and RdpBackendSelector with its detection caching) was a relic of an older backend-selection design and had no production caller: the live FreeRDP-client choice goes through embedded_rdp::detect and protocol::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_via path 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 the gnome extension, which was the blocker that reverted the 0.23.2 attempt. The snap now builds on Ubuntu 26.04 against the gnome-core26 platform (GTK 4.24, libadwaita 1.10) with GPU drivers from mesa-2604, and is compiled with adw-1-8 like 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 new gnome-core26 and mesa-2604 platform 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 the gsettings plug the snap declares it explicitly. The bundled FreeRDP client stays freerdp3-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 update cleared a yanked-crate warning (cargo deny check advisories flagged 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.json regenerated 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.RustConn

Snap

sudo snap install rustconn

Debian/Ubuntu (.deb from this release)

sudo dpkg -i rustconn_0.24.0_amd64.deb
sudo apt-get install -f  # Install dependencies if needed

Fedora (.rpm from this release)

sudo dnf install rustconn-0.24.0-1.fc44.x86_64.rpm

AppImage

chmod +x RustConn-0.24.0-x86_64.AppImage
./RustConn-0.24.0-x86_64.AppImage

macOS (Homebrew)

brew tap totoshko88/rustconn
brew install rustconn
open $(brew --prefix)/opt/rustconn/RustConn.app

All 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 rustconn

Arch Linux (AUR)

yay -S rustconn

FreeBSD (Ports)

pkg install rustconn

Full 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 SHA256SUMS

Signed 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.)