Skip to content

v0.21.6

Latest

Choose a tag to compare

@github-actions github-actions released this 05 Sep 13:21

What's Changed

Added

  • An optional second confirmation before a snippet runs (issue #315) — a Confirm before running switch in the snippet editor. With it on, running the snippet opens an adw::AlertDialog showing the fully substituted command and waits; with it off, which is the default and what every existing snippet deserializes to, nothing changes. The field is Snippet::confirm_before_run, serialized only when true, so snippets.toml written by this version still loads on older ones. The interesting part was not the dialog but the count of ways a snippet can already be started: the picker, the Execute button in the snippet manager, the variable-input dialog's own Execute button, the inline snippet items in the terminal's right-click menu, and the Scripts menu of an embedded RDP session. The first four each reached VTE through their own copy of the resolve-then-send sequence — execute_snippet, execute_snippet_direct and the variable dialog's handler all called send_text_to_focused independently, and that helper takes a &str, so it never knew which snippet it was sending. A gate in any one of them would have left the other three open. They now funnel through one send_snippet_command, which is the only thing in the module that appends the newline that makes a shell run the text, so a future call site that forgets the flag would also have to reinvent that. execute_snippet_direct gained a parent-window parameter to make this possible: it had none, on the stated grounds that a context-menu action has no window to offer, but setup_snippet_actions holds the window and the three sibling actions in the same function were already downgrading it. RDP is a genuinely separate delivery mechanism — clipboard-plus-Ctrl+V or per-character autotype, not a VTE write — so it gets its own gate around the same command text, on the reasoning that where the keystrokes come from does not change whether the user meant to send them. The confirmation from the variable dialog is parented on that dialog and closes it only once the command has gone out, so cancelling returns the values you typed instead of discarding them. The button is styled destructive: the user turning this on for a specific snippet is a statement that running it by accident is expensive, and the default response stays Cancel either way. Both gates log at debug level, which the snippet execution path did not do at all before — running the app with RUST_LOG=debug and executing a snippet produced no output whatsoever, so a snippet that failed to arrive was indistinguishable from a click that never reached the app, which is the same blind spot the 0.21.5 tray-menu fix was found through. The command itself is deliberately not logged, only its length: substitution has already happened by that point, so the text can carry values resolved from vault-backed global variables. The cancel branch is logged too, since declining is a state the flag creates and nothing else would record it.
  • rustconn-cli snippet honours the same flag--confirm on add, --confirm [<true|false>] on edit (the value is optional and defaults to true, and omitting the flag entirely leaves the current setting alone), and snippet show reports it. The part that matters is snippet run --execute, which runs the command through sh -c: it now prompts on stderr, and with no terminal on stdin it refuses outright rather than prompting into a pipe, so a snippet marked for confirmation is never executed unattended by accident. --force is the opt-out for scripts that mean it, mirroring connection delete --force. Without this the flag would have been advice the GUI took and the CLI ignored, on the one path in the workspace that actually spawns a shell. Declining and having nobody to ask now say different things — one is a decision, the other is a script that needs --force.

Fixed

  • Bitwarden auto-unlock did nothing at all in every interface language but English (issue #312, reported by @vh45f) — the vault could be unlocked by hand from Preferences ▸ Secrets and then worked, but the unlock RustConn performs for itself at startup, from the master password in the keyring, was skipped. check_bitwarden_status_sync returned the display string for the vault state, so it had already been through i18n(), and both callers then decided whether to attempt an unlock by comparing that string against the literal "Locked". In Italian the string is Bloccato, in Ukrainian Заблоковано; neither equals "Locked", so the guard read "not locked", returned early, and set the status row to the locked text it had just declined to act on. English was the one locale where that comparison could be true, which is why this survived testing on both the system and Flatpak builds. The reporter's log shows it exactly: the auto-unlock step "completed" in 2472 ms, which is the cost of one bw status in that same log, where a real unlock takes about 4.5 s. The state is now an enum, BwVaultStatus, and the decision is should_try_unlock() on the variant, with the label rendered separately at the point it is shown. Two other places in the same paths pushed a bare "Unlocked" or "Locked" into that status row untranslated; they go through the same renderer now. The decision also fires on an inconclusive probe, which is a second door onto the same silence: bw status gets five seconds and cost 2.5 s in that same log, so a slower link answers ProbeFailed, and reading that as "nothing to do" skips the unlock exactly as the string comparison did. An attempt against a vault that turns out to be unlocked costs one bw unlock and returns a fresh session key, so guessing wrong there is cheap; Unauthenticated still declines, because no password unlocks a CLI with no account. Tests pin the decision per variant and pin the shape of the original bug — that a decision taken from the rendered label is wrong even in English, because the label is what a translator is free to change.

  • The startup banner announced that Bitwarden could not store passwords while Bitwarden was storing them (issue #312) — "Bitwarden is selected, but it cannot store passwords yet: Locked", on a vault that had unlocked, synced and was answering lookups seconds earlier in the same log. The readiness probe ran bw status as a bare std::process::Command, and RustConn deliberately keeps the session key in process memory rather than in its own environment — so the child was launched without BW_SESSION, bw status could not see the session, and it answered locked, correctly. That answer became BackendReadiness::NeedsAction, which is what the banner renders. The probe now assembles its command the way BitwardenBackend::build_command does: the session key from get_session_key() passed through the environment rather than argv, the extended PATH a sandboxed bw needs to find the tools it shells out to, and --nointeraction, so a call with a five-second budget cannot stall on a prompt or an implicit network fetch. Worth recording because 0.21.3 was reported as fixing this and did not: that release corrected the banner's wording, which until then claimed a missing keyring client for a product that has no keyring client. The verdict behind it was still wrong.

  • A Bitwarden password was written to the vault and reported as refused at the same time (issue #312) — "Failed to store credentials: Vault store timed out after 10s", after which the password was in fact present in the vault and RustConn offered to save it somewhere else instead. One Bitwarden store is three bw processes in sequence — list folders, then list items, then create item or edit item — each a fresh node plus a round trip to the vault server. Measured in the reporter's log against bitwarden.eu: 2.9 s, 5.0 s, and create item still in flight when the budget expired at 10.008 s. The ten seconds came from a comment about a hung keyring blocking a GTK callback, which is a real concern for a D-Bus call and not the same order of magnitude as a CLI over a network. Dropping the future is also not the same as stopping the work — tokio::process does not kill on drop — so bw ran to completion and the write landed: the failure was in the reporting, not in the store. The budget is now chosen per backend by vault_op_timeout, 45 s for the four CLI-backed backends and the previous 10 s for everything that answers from this machine, and it applies to reads and deletes too: a bw list items alone took 5 s there, so a saved password could as easily have gone unfound on connect and been asked for again. Each bw invocation keeps its own 30 s ceiling inside rustconn-core, and none of these calls run on the GTK thread, so the longer wait costs a slow save rather than a frozen window. The timeout message now states the budget that was actually applied instead of a hardcoded "10s". The credential-transfer loop keeps its own, smaller per-entry budget: that one is spent forty times across a batch and is a different trade-off.

  • Add SSH Key did nothing, and there was no way to find out why — Preferences ▸ Secrets ▸ SSH Agent ▸ Add Key opens a GtkFileDialog, and its callback matched the outcome with if let Ok(file) = result, so every failure was discarded without a word. That is also how a dismissed chooser arrives, which is why the shape looked deliberate, but it swallowed real errors identically — including a desktop portal that refuses the request, which is what a GtkFileDialog failing to open usually is. The whole path had no logging either, so the button was indistinguishable from a button with no handler at all. Dismissal is now told apart from failure: closing the chooser logs at debug and does nothing, while an actual failure logs a warning and opens a dialog carrying the system's own message, pointing out that keys already in ~/.ssh are listed under Available Key Files and can be added without the chooser. A chosen location with no local path — a remote mount, which ssh-add cannot use — is reported rather than ignored, and the two "no root window" paths now say which one happened instead of both logging the same line. This makes the failure diagnosable; the underlying reason it fails on a given system is whatever the new message names.

  • The Add Key passphrase dialog had no visible way out — Preferences ▸ Secrets ▸ SSH Agent ▸ Available Key Files ▸ + opened a dialog with a single Add Key button, no Cancel, and the header's close button explicitly hidden. Escape and clicking outside did dismiss it, so it was never a trap, but nothing said so and it read as stuck. The cause is that this dialog did not use any of the house patterns: dialogs/widgets.rs::dialog_header hides the title buttons and puts the action in the header, which is why hiding them is normally fine, whereas this one hid them and put its only button in the body. It now matches portable_passphrase_change and credential_transfer, the two other dialogs that take input and hide the title buttons — Cancel at the start of the header, the action at the end. Pressing Enter in the passphrase field also submits now, as it already did in the connection password dialog. The two dialogs mentioned above were checked for the same defect and do not have it.

  • Global variables that could not be written to disk were reported as saved — the Variables dialog updated the settings in memory, so the edits looked applied and survived until the next start, at which point they were simply gone; the only record was a tracing::error!. This is a save failure on user data, so it now opens a dialog naming what went wrong, with a toast as the fallback for when the window has already closed. The same file was already doing this correctly forty lines above, for a failed vault write, and with the same reasoning written out — the disk write was the one path that had been left as a log line.

  • A standalone SSH tunnel could fail in complete silence, and the diagnosis was already being built and then thrown away — pressing Start with ssh absent logged a warning, redrew the row as Stopped and told the user nothing; a tunnel that died later simply left the Active group, which looks exactly like having stopped it on purpose. The information existed the whole time. TunnelManager captures the ssh process's stderr in a background thread, and health_check formatted it into TunnelStatus::Failed(msg) — then removed the entire process record one loop later, discarding the message, after which status() answered Stopped. tunnel_builder::path_diagram has a TunnelStatus::Failed branch already written to draw exactly that text, which could therefore never fire, and the public TunnelManager::stderr() had no callers at all. health_check now returns what it found as TunnelFailure { id, reason } and records the reason, so status() reports Failed for a tunnel that exited on its own and Stopped only for one never started or stopped deliberately; both clear on the next explicit start or stop. The row shows the difference with a warning icon, an error style and an accessible label rather than colour alone, plus a selectable Last Error row carrying ssh's own words — "Permission denied", "Address already in use" — placed in the expanded body instead of the subtitle, because stderr is arbitrarily long and would wreck the collapsed row. A failed start now opens a dialog, since a start the user asked for and did not get is a half-finished action rather than a background event, while a tunnel dying on its own raises an error toast. A missing binary gets its own error variant carrying the program name, because an MPTCP-enabled connection runs mptcpize rather than ssh and a message naming ssh would have sent the user to install something they already had. Auto-reconnect also stopped discarding its Result — that is what let a reconnect which could never succeed retry in silence until the attempt counter ran out — and giving up after the final attempt now says so instead of only logging it. Two follow-ups from auditing that fix are below: the remedy it printed named the wrong package, and none of it was visible while the tunnel manager was open.

  • A failed tunnel start named the wrong package to install — the entry above added ProgramNotFound carrying the program name for one stated reason: an MPTCP-enabled connection runs mptcpize rather than ssh, so a message naming ssh would send the user to install something they already have. The remedy sentence then said "Install the OpenSSH client" whatever the name held, producing "needs mptcpize … install the OpenSSH client" — the same misdirection, one line below the comment explaining it. The advice follows the program now: ssh points at the OpenSSH client, mptcpize at the Multipath TCP tools with the package name most distributions use, plus the alternative of turning Multipath TCP off for that connection. An unrecognised name gets generic advice rather than a guess, because the string arrives from rustconn-core and a third carrier added there would otherwise show up with a confidently wrong package name.

  • The tunnel manager did not notice a tunnel dying while it was open — the health check runs on a five-second timer and had no way to reach the dialog, so a tunnel that exited kept its row in the Active group saying Running. The warning icon, the accessible label and the Last Error row — the entire visible half of the fix above — appeared only after the dialog was closed and reopened, or after some unrelated button happened to trigger a refresh. The one window built to show a tunnel failure was the one place it did not show. The health check now redraws an open manager, through a handle the dialog publishes when it is presented: held weakly, since a strong reference would keep a closed dialog alive for the life of the process, and a no-op when nothing is open, which is what makes it safe on a timer.

  • Running a snippet from the terminal's right-click menu could do nothing at all — a snippet with a variable that neither a global variable nor its own default could supply was dropped without a dialog, a toast or a log line. The comment explaining that pointed at the "Execute Snippet…" picker, because the variable dialog needs a parent window and a context-menu action was said to have none — but it has had one since the confirmation gate above needed somewhere to anchor. The reason outlived the constraint it described, and what remained was the one route where choosing a snippet from a menu silently did nothing. It opens the variable dialog now, with whatever did resolve pre-filled. Fixing it exposed a divergence between the two resolution loops: execute_snippet collected every unresolved name while execute_snippet_direct stopped at the first, so the same snippet would have reached the same dialog with different fields pre-filled depending on which menu you came from. They share one resolve_snippet_variables now.

  • The embedded RDP confirmation showed the command untruncated — the terminal gate caps the preview at 400 characters because an adw::AlertDialog grows with its body until it stops being readable; the RDP gate passed the command whole, so a generated one-liner made the dialog unreadable on exactly the snippets a confirmation is worth having for. Same dialog, same failure, same helper.

  • Add Key could be left permanently unable to open a file chooser — the guard against a second click while a chooser was open disabled the button and re-enabled it in the chooser's callback. That callback fires when the user picks a file or closes the chooser, so it can be minutes away while they browse, and there is no "the chooser appeared" signal to time out against instead. In the environment recorded in the previous entry — a portal that refuses the request — the callback never fires at all, so the button stayed dead for the rest of the session with no message, which is a worse version of the report this path exists to fix. A stacked pair of choosers is recoverable; a dead button is not. The button stays live now and a new click cancels the request in flight, which arrives at the callback and is recognised there.

  • Every bw unlock ran without a deadlineunlock_vault_sync spawns up to three children and bounded none of them, so an unlock stalled on a network sync blocked its caller indefinitely, and every caller is a worker thread with a status row waiting on it. The 30-second ceiling that run_command already had was a literal in one place, which made "each bw invocation has a deadline" true of exactly one invocation; it is a named constant now and all four honour it, through the helper that also reaps the child it kills. The two --passwordenv attempts additionally pass --nointeraction, matching how the backend builds every other command; the stdin fallback deliberately does not, since it exists for older CLI builds and works by answering the prompt that flag suppresses. Two smaller things a security pass found while this was written, both pre-existing: the unlock logged the master password's length, which is bruteforce metadata that the GUI's own handler explicitly declines to log before sending all four of its call sites here; and raw bw stderr flowed into a debug log and into the error the Secrets page renders, while the session-key parser only ever reads stdout — so a build emitting its export BW_SESSION="…" banner on stderr would have leaked the vault's bearer token down the one path that never looks for it. Both are closed, with the reason text ("Invalid master password", "not logged in") deliberately preserved because two callers match on it.

  • RUST_LOG=debug wrote the RDP account password into the log in clear — found while reading a debug log attached to a bug report, which is exactly where it does the most damage. sspi logs the encoded CredSSP TSRequest at debug level, and field [2] of TSPasswordCreds is the password as plain UTF-16LE, so the log carried a decodable byte array: an ASN.1 walk over the array from that report yielded a 26-byte userName and a 64-byte password, which is 32 printable characters. What made it easy to miss is that the same log line prints password: Secret in its span fields — the secrecy wrapper was working perfectly on the field RustConn owns while the crate underneath it serialised the whole structure a few characters later. A sspi=warn directive now joins the three ironrdp* ones, matching how those are already treated. It is added after EnvFilter::from_default_env() and is therefore deliberately not overridable: a RUST_LOG=sspi=debug no longer re-enables it, because nobody should be able to turn a credential leak back on by accident. Anyone who has run 0.21.5 or earlier with RUST_LOG=debug against an RDP host should treat that account's password as disclosed.

  • A clean shutdown reported that every terminal child had ignored SIGTERM — quitting with sessions open logged "child ignored SIGTERM, killing process group" once per session, which reads as a hung ssh needing a SIGKILL, and is not what happened. still_our_group_leader exists to prevent signalling a pid that has been recycled onto somebody else's process, and it answers true for a zombie — a child that has already exited and is waiting to be reaped. On shutdown that is the normal state: the reaper is GLib's child watch, and its main loop is gone by then, so a child that obeyed the signal promptly is still in the process table when the grace period ends. The check now tells a corpse from a live process by reading state Z from /proc/<pid>/stat, and the two escalation sites report which one they found. The SIGKILL to the process group stays in both branches, deliberately: an ssh with a ProxyCommand shares its group with helpers that the parent's exit does not reach, so sweeping the group is right even when the leader is already dead — only the message was wrong. Extending the grace period was considered and rejected, since nothing was ignoring anything; so was reaping with waitpid(WNOHANG), which would steal the child from a GLib watch that may still be running. The original reading of this log was wrong in a way worth recording: status=65280 is WIFEXITED with code 255, which is ssh's own exit, not a signal at all.

  • An unset console keymap made every RDP session US Englishlocalectl status prints VC Keymap above X11 Layout, and on a desktop the console keymap is normally unconfigured, so the output opens with VC Keymap: (unset) and only then gives X11 Layout: de. The parser returned the first line matching either label, answered (unset), found no Windows keyboard-layout identifier for it, and logged "Keyboard layout detection failed, using US English". Detection had not failed. It had found the layout and discarded it one line later. The consequence is silent and hard to attribute — the server interprets every scancode against the wrong table, so a German or French keyboard types the wrong characters in an embedded RDP session with nothing in the UI pointing at the layout. On a us,* machine the wrong answer happens to equal the right one, which is why this survived. X11 Layout now wins wherever it appears, and the placeholders systemd prints for an unset value are rejected rather than read as layout names — (unset) today, n/a in older releases. Both sources also report the whole group list rather than a single name, so a machine whose primary layout is unknown to the table no longer falls back to US English while naming a layout that is in the table one entry later. The parse moved out of the spawn, so those shapes are tested without a localectl on the machine running the tests, and the fallback message now carries what each source answered and points at the connection's explicit keyboard-layout setting.

  • With LibSecret, a password saved on a connection in no group was never found again (issue #316, reported by @Xiaol1n173) — connecting prompted for a password that was sitting in the keyring, and the connection dialog's 📂 load and ✓ test buttons both reported it missing. Putting the connection into a group fixed everything, which is the shape of the clue: generate_store_key_with_group takes an Option<&str> group path with three meanings, not two. Some("Production") is a grouped connection, Some("") is an ungrouped connection — still prefixed RustConn/, because that is what its entry is written with — and None is the flat pre-0.19.18 key that belongs to no connection at all. Four call sites built that Option themselves, and they disagreed: saving and deleting passed Some(""), while resolving and the two dialog buttons wrote connection.group_id.map(…), which is None when there is no group. So the password went to RustConn/{name} ({protocol}) and was looked for at {name} ({protocol}). The Secret Service matches attributes exactly, so the two never met. A grouped connection escaped it for a reason worth stating: there the two keys differ, so the resolver tried its legacy second key as well and that one hit — the bug needed the hierarchical and flat keys to collapse onto the same wrong string, which happens only when there is no group. The reporter's own conclusion is the fix: the ungrouped decision now lives in one function, generate_store_key_for_connection, which takes the group id rather than a pre-joined path, and all four sites call it. Save, resolve, delete, migrate and the credential transfer therefore cannot drift apart again the way the previous half of this fix did in 0.21.0, which corrected the resolver for grouped connections and left the ungrouped case behind. What was missing was the test, not the insight: nothing compared the key the GUI writes against the key the resolver reads, and every existing test used a grouped connection. Four now pin it, including that Some("") and None are not interchangeable. One asymmetry found while reading and deliberately left alone: the store key does not run the connection name through sanitize_imported_value and the resolver's does, so an imported name ending in a literal escape can still produce two strings — the delete path already carries both keys for exactly that reason, and narrowing it belongs with the import code rather than here.

  • The shutting-down flag was set after the session exits it exists to explainAPP_SHUTTING_DOWN is how a session-exit callback tells an exit the application caused from one that is a fault, and it was set in connect_shutdown. GTK runs that from app.quit(), which do_quit calls after shutdown_sessions_for_exit has already signalled every session child, synchronously — so on the ordinary quit path both guards that ask the question got false. One of them decides whether a failed terminate_session is a debug line or a warning, and was left resting on its second clause alone; the other is the early return that stops a reconnect banner and a failure toast being raised for a session the teardown had just killed, and it was simply unreachable. The flag is now set at the top of shutdown_sessions_for_exit, the single teardown every exit path funnels through, and one a tray-minimize never reaches — which is what makes it the right place, since the flag must not be set by a close that leaves the application running. connect_shutdown still sets it as well, for an exit that never reaches the helper. The post-disconnect task also gets a guard it never had: it runs on a detached worker thread and reports through a 16 ms main-loop callback, so at shutdown the process exits from under the thread mid-command and the loop that would deliver the result is already stopped — neither the success nor the failure arm can log. It was being started and abandoned, an arbitrary user command interrupted halfway with nothing recording the attempt. It is skipped now, and says so, because a task that silently did not run is the same puzzle from the other side. Three comments blamed close_all_control_sockets() for the expected exits; it closes SSH ControlMaster sockets from connect_shutdown, after the children are already dead, so they name the teardown instead. One thing checked and left alone: Session already gone; nothing to terminate appearing once per open session is correct — the handler is keyed by session and hook since issue #297, so one exit runs it once, and three lines meant three sessions rather than three teardown paths.

  • A second Quit stacked a second confirmation instead of raising the first — the log from a tray quit shows msg=Quit at 22:40:44, msg=Quit again at 22:40:54, and the teardown only at 22:40:58: the first click looked ignored. It was not. It raised the close confirmation, and the second click built another one on top of it. close_confirmation_dialog is called from two independent places — the main window's close_request, and the app.quit action, which is where Ctrl+Q, the primary menu and the tray's Quit item all arrive — and neither asked whether the question was already on screen, so every activation constructed and presented a fresh dialog. They stack across call sites too: the window's × followed by Ctrl+Q produced one from each. Both now go through one helper that returns the dialog to wire up, or nothing when a confirmation is already up — in which case it raises the window carrying it, since an AdwDialog is drawn inside its host window and raising that window is what puts the question back in front of a user whose first ask went to a window sitting behind another, on another workspace, or hidden to the tray. Nothing is ever read as consent: close_request still stops the close and the quit action still returns without quitting, so whichever dialog is pending owns the decision. The guard holds the dialog weakly — a strong handle has to be released by a signal, and a guard that can outlive what it guards is how a control ends up permanently dead, which is why the click guard came back off Add Key two entries above. The quit action also had no logging whatsoever, so the log could not distinguish a raised dialog from an action that did nothing; all three outcomes now say which one happened.

Improved

  • The four hand-rolled bw unlock invocations on the Secrets page are gone — each built its own command, and between them: none passed the extended PATH a sandboxed bw needs, none passed --nointeraction, none had a deadline, and only two of the four implemented the verbose fallback for CLIs that do not support --raw. They all call unlock_vault_blocking now, so the fix above applies to every one of them and a fifth call site cannot reintroduce the gaps. The session key travels back as a SecretString instead of a bare String that each site re-wrapped, the GUI's duplicate copy of the session-key parser is deleted — it never sees the key as text at all now — and a failed unlock is logged, which is what previously made it indistinguishable from one that was never attempted.

  • One confirmation prompt in rustconn-cli instead of three — adding the snippet prompt made a third near-copy of the same fifteen lines, and they had already drifted: history clear printed its question to stdout, which is why it needed an explicit flush the other two did not, and which put a prompt into whatever pipe was collecting the command's output. What kept the copies apart was not the prompt but the answer — connection delete treats a non-interactive stdin as a silent abort, while history clear and snippet run --execute fail and name --force. A bool cannot carry that, so each call site kept its own copy to keep its own behaviour; the shared helper reports three outcomes instead, and "nobody to ask" is never consent.

  • OpenH264 was probed again on every RDP connection — the embedded client does not link a decoder, it dlopens one, and each connection walked the whole candidate list from scratch: OPENH264_PATH, the well-known system paths, then the versioned-soname scan, stating each and dlopening every file that existed. On a machine with a distribution libopenh264 the loader refuses it on the hash check, so each walk also re-emitted the same warnings — three connections in one log produced nine identical lines explaining that the library is not one of Cisco's published binaries. That is a property of the machine, it cannot change while answering the same question, and it was being repeated at the one severity users actually read, which is how a real warning gets lost. The decoder itself still cannot be shared, since each session needs its own — so what is cached is the outcome, the path that loaded or nothing, and a fresh decoder is built from it per session. A failure to build one from an already-proven path is now a warning rather than a silent fall back to RemoteFX, because it means a second session failed where the first succeeded. The trade is that installing a Cisco blob into a running RustConn is not picked up until restart; the answer depends on files and an environment variable that do not change under a running process, and the alternative is paying the walk forever.

  • A KeePass lookup paid for one database open that could never match — connecting to a host whose password is not in the database took 2.9 s before the prompt appeared, and the log accounts for all of it: three keepassxc-cli invocations against three candidate entry paths, roughly 700 ms each. That cost is inherent, because every run of the CLI reopens the KDBX and pays its Argon2 cost again — the only thing available to reduce is the number of opens. One of the three could never have succeeded. For a connection inside a group the candidates were RustConn/Production/nginx-01 (rdp), then the same without the protocol suffix, then Production/nginx-01 (rdp) "without the RustConn prefix" — but KeePassHierarchy::build_entry_path starts every path it builds at RustConn, so no release has ever written a grouped entry at the database root. The un-prefixed form can only match the ungrouped case, where the name is a bare entry name, and there it stays. A path the user chose does not come through this function at all; that is get_password_from_kdbx_exact, which queries it as-is. The construction moved into a pure function so the order is finally pinned by tests: exercising the loop needs a keepassxc-cli and a real database on the machine running the tests, so until now the sequence was only ever verified by reading it. Separately, find_keepassxc_cli remembers where it found the binary — every reader and writer in the module called it and each call redid the search, a PATH walk plus up to six stats natively and, inside a Flatpak sandbox, an extra child process running sh -lc 'command -v …' on the host. Only a successful find is cached: the tidier OnceLock<Option<_>> would also remember failure, and since the Flatpak probe has a two-second budget, one slow probe would leave the rest of the session convinced KeePassXC is not installed with a restart as the only cure.

Documentation

  • Three claims that were not true of the codedocs/CLI_REFERENCE.md said snippet run resolves ${VARIABLE} from Global Variables before execution. It does not, and cannot usefully: a global variable may be vault-backed and the CLI has no session to unlock it with, so a name it cannot fill is left in the command text for the shell to expand, usually to nothing. rustconn-cli/AGENTS.md required every user-facing string to go through i18n(), in a crate that has never contained a single i18n call — so the rule was being "followed" by nobody, and following it for one new string would have produced output in two languages. It now says the crate is English-only, that this is a gap rather than a decision, and what translating it would actually involve. And the comment listing the reachable routes to the snippet picker omitted the terminal context menu's own "Execute Snippet…" entry, which is that action's main caller.

  • The debug-logging instructions did not say what a debug log disclosesdocs/USER_GUIDE.md told users to run with RUST_LOG=debug and attach the output to a bug report, which is good advice and was incomplete: a debug log names every host, username and connection you opened, and until the password leak fixed above it also carried the RDP account password itself. The section now says so before the command rather than after it, so the warning arrives while there is still a decision to make.

Dependencies

  • Updated: cc 1.4.4→1.4.5, find-msvc-tools 0.1.11→0.1.12, indexmap 2.14.1→2.14.2, js-sys 0.3.104→0.3.105, syn 3.0.4→3.0.5, tinyvec 1.12.0→1.13.2, tokio-rustls 0.26.4→0.26.5, wasm-bindgen 0.2.127→0.2.128 together with its -futures, -macro, -macro-support and -shared crates, web-sys 0.3.104→0.3.105, zstd-safe 7.2.4→7.3.0, zstd-sys 2.0.16→2.1.0. Six of those fifteen are the wasm-bindgen family, and nothing this project ships compiles them: cargo tree --invert wasm-bindgen finds no path to it on the host target and answers "nothing to print", so they are reachable only behind a wasm32 target and are listed here because Cargo.lock moved, not because a binary did. indexmap is the one with real reach — it arrives three separate ways, through h2/hyper/reqwest, through serde_yaml_ng, and through toml_edit behind the GTK macro crates. Thirteen further crates remain behind their latest release because the newer versions are semver-incompatible, so cargo update does not reach them; that count is unchanged by this run. cargo deny check advisories is clean against the new lockfile, and both cargo-sources.json files were regenerated from it, so the Flatpak and Flathub builds fetch the same crate set the workspace resolves to.
  • FreeRDP (Flatpak) 3.31.0 → 3.31.1 — upstream calls it a papercut release after the security run that 3.31.0 belonged to, so there is no advisory behind this one, but three of its fixes land on the exact client this project builds. RustConn compiles FreeRDP with -DWITH_CLIENT_SDL3=ON, and 3.31.1 fixes flickering in that client on displays with a scale factor other than 1.0, maps a set of keys the SDL client previously dropped (including the Japanese YEN/RO/EISUU/KANA keys), and makes WinPR read JSON files in binary mode — that last one is the sdl-freerdp.json path the bundled cJSON module exists to enable, which is how the RDP hotkey configuration is applied at all. The sha256 was taken from upstream's own published checksum beside the tarball and matches the archive as downloaded. Only the two Flatpak manifests bundle FreeRDP; the deb and RPM depend on the distribution's copy.
  • The Flathub build had no H.264 decoder at all, so the embedded RDP client fell back to RemoteFX on every session that offered H.264 — the client does not link a decoder, it dlopens one at runtime: gfx_handler.rs probes /app/lib/libopenh264.so and then scans /app/lib and /app/lib64 for a versioned soname. The openh264 module supplying it was added to packaging/flatpak/io.github.totoshko88.RustConn.yml when the IronRDP GFX handler landed, and to neither of the other two Flatpak manifests — so the probe found nothing on Flathub, which is the build most users install, and it also found nothing in a local flatpak-builder run, which is the build that exists to reproduce what Flathub ships. Silent degradation to RemoteFX is the exact failure gfx_handler.rs's own comments warn about for a runtime-only distro install; nobody looked for it in the packaging. All three manifests now carry the same openh264 2.6.0 module and the same comment explaining what it is for, because an H.264 module sitting next to FreeRDP in the module list reads like a FreeRDP dependency and is not one — flatpak-builder builds modules in order and openh264 comes after FreeRDP, so FreeRDP's configure step never sees it. Bundling is the only route left: freedesktop dropped org.freedesktop.Platform.openh264 from the runtime in 2025, so there is no extension to depend on.
  • CLI downloads — unchanged. TigerVNC is the only pinned component and its pin, 1.16.2, is upstream's current release; the other twelve resolve the latest version at runtime and never need a bump.
  • Checked and already current: the other nine bundled Flatpak modules — fast_float 8.2.10, VTE 0.80.5 (newest in the 0.80 series the pin allows), inetutils 2.8, picocom 3.1, S-Lang 2.3.3, mc 4.8.33, waypipe 0.11.2, cJSON 1.7.19 and openh264 2.6.0 — are each at upstream's latest release, so only FreeRDP moved.

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.21.6_amd64.deb
sudo apt-get install -f  # Install dependencies if needed

Fedora (.rpm from this release)

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

AppImage

chmod +x RustConn-0.21.6-x86_64.AppImage
./RustConn-0.21.6-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