v0.19.12
What's Changed
Added
-
Terminal history now survives a reconnect (issue #253) — an in-place reconnect (SSH, Telnet, Serial, Kubernetes, Mosh and custom-command sessions) now keeps the previous session's scrollback, with a dim
── Reconnected at … ──separator opening a fresh line so the preserved output and the new session stay apart.Previously, reconnect called
reset(true, true), whose second argument empties the scrollback — so a session dropped by a server idle timeout came back to an empty terminal. Two details had to come with the fix:resetonly switches back to the normal screen in itsclear_historybranch, so a session that died inside a full-screen app (vim, htop, less) would have kept showing that app's frozen screen —DECRST 1049is now fed after every history-preserving reset, the one at disconnect included, which also makes the scrollback readable while the tab sits disconnected. And the viewport returns to the bottom, because the user may have scrolled up to read the dead session. "Keep on reconnect" in Settings → Terminal → Scrolling turns the whole thing off; it is on by default. -
Automatic login for Telnet and serial sessions, with configurable expected prompt text (issue #254) — Telnet and Serial sessions now log in by typing the account name and password at the device's own prompts. The credentials come from the connection's Username and Password Source; a connection with neither set is untouched. Each step fires exactly once — a device that re-prompts after a rejection is handed back to the user, because automatic retries are how an account gets locked out.
Because vendors word prompts inconsistently (
>>User name:on a Huawei OLT MA5800,Username:on an S6700,login:on a Datacom), the Automation tab gained an Automatic Login group with Username Prompt and Password Prompt fields, matched as a case-insensitive substring rather than a regex. All common forms (login:,login as:,user:) are recognized with both fields left empty, so the fields are the exception rather than the setup step. The same two fields exist on a group (Edit Group → Automation) and are inherited field by field down the group chain; on the CLI:rustconn-cli group edit --username-prompt/--password-prompt.Last login:in an MOTD is explicitly not a username prompt. -
A session can leave a split view without being closed (issue #252) — a session shown in a split pane has no standalone tab of its own (it is parked when it enters the layout), so the only way out of a pane was the × button, which terminates the session. There was no way back to a single tab short of closing everything the split held. Two actions now do it without touching a single connection. Remove from Split (
Ctrl+Shift+R, a button beside × on every occupied pane, and the pane's context menu) hands the focused pane's session back to its own tab; Remove Split (Ctrl+Shift+J, the pane context menu, and the context menu of the tab that hosts the split) dismantles the whole layout and returns every session in it to a tab. The live widget is reparented rather than rebuilt — the samereparent_terminal_to_tabpath a split already used when it collapsed on its own — so the PTY, the child process, the scrollback and an embedded RDP/VNC viewer's connection all survive the move, and monitoring, suspended when the session entered the split, resumes against the new container. Asking for the pane that owns the layout collapses the split instead, because the split widget lives in that tab and would be left without a host. -
Configurable auto-login timeout — the 10-second deadline the auto-fill watcher uses before giving up is now a per-connection (or per-group) field: Login Timeout on the Automation tab, inherited down the group chain. Network equipment with slow POST/boot (Cisco ASR, Huawei MA5800 via serial) can set 30–60 s; the default stays 10 s. On the CLI:
rustconn-cli connection edit --login-timeout-secs 30. -
Maximum scrollback on reconnect —
max_scrollback_on_reconnectinconfig.toml(under[terminal]) limits how many lines of previous-session output survive a reconnect. Without it, a connection that idle-timeouts every few minutes and auto-reconnects would grow the buffer without bound. When set, VTE's scrollback cap is temporarily lowered before the reconnect separator is inserted, trimming the oldest lines. Not exposed in Settings UI yet — editconfig.tomldirectly.
Fixed
-
${password}in an expect-rule response resolved to nothing (issue #257) — the stock "Sudo Password" template answers[sudo] password for …:with${password}\n, the Automation tab offers${password},${username},${host}and${port}under a "Built-in" heading, and the user guide called the first one "the connection's password from the configured secret backend". None of the four existed.prepare_rules_from_configsubstituted against aVariableManagerseeded exclusively fromsettings.global_variables, andsubstitute_for_commandreplaces an undefined reference with an empty string, so the template sent a bare newline — sudo answered "Sorry, try again", the rule was spent (one_shot), and typing the same password by hand a second later worked. Reaching for a global variable instead was the documented workaround and failed too, for a second reason:substitute_for_commandexists to build shell arguments and rejects any value containing; | & \$ ( ) < > !, whereupon the caller fell back to the raw template and typed the literal${pw}into the session. All four placeholders are now supplied from the connection at connection time bywindow::protocols::automation_variables— the password from the same credential cache the SSH, Telnet and Serial auto-fill reads, so nothing lands inconfig.toml— and substitution goes through a newVariableManager::substitute_for_terminal_input, which validates for a PTY rather than for a shell: metacharacters pass untouched, while a NUL, a line break or another control character is rejected, because a newline inside the value would submit the answer before the rest of it was typed. A password is only copied into aVariablewhen an enabled rule actually references${password}`, and that variable is marked secret so it is zeroized on drop. Wired into SSH, Telnet, Serial, Kubernetes, Mosh and Zero Trust; a built-in shadows a global of the same name for that connection only, which keeps the workaround working for anyone who set one up. -
A backslash in a resolved value was reinterpreted as an escape sequence —
\n,\tand\\in an expect response were expanded at match time, i.e. after substitution, so the expansion also ran over whatever a variable had resolved to. A password containing\nwas sent as a line break, splitting it in two and submitting the first half; one containing\slost the backslash. Escapes are now expanded on the template first and substitution happens second, leaving resolved values alone. -
An expect rule whose response could not be substituted typed the placeholder into the session — the fallback for a failed substitution was the unsubstituted template, so a rule meant to answer a password prompt sent the characters
${pw}to the remote host. Such a rule is now skipped with a warning naming the variable, which leaves the prompt for the user to answer. Unresolved references in an otherwise valid response are logged by name too — never by value, sincetracingoutput is not redacted. -
Connect detection and prompt detection read the oldest scrollback instead of the screen (issue #253) —
vte_terminal_get_cursor_positiondocuments its row as absolute, counted from the start of the scrollback rather than the top of the screen, and bothget_terminal_textand the fallback incursor_line_textaddressed rows0..row_countas if they were the visible grid — the same trap the highlight overlay had to fix in #154. Latent while a reconnect always started from an empty buffer, each one breaks the moment it does not: the "cursor advanced past the connect banner" check that flips a session to connected (every VTE protocol plus Local Shell) would have fired on the preserved scrollback alone, painting a failed reconnect green, and the jump-host failure scan and password-prompt fallback would have matched against the oldest lines in the buffer rather than what is on screen. The cursor row is now reported relative to the row the current connection started on — zero for a fresh session — and both text helpers are anchored to the visible window. -
An expect-rule response was written to the application log in clear text —
AutomationSessionlogged every rule's response at INFO on session start and again on each match (rule.response.escape_debug()). The redaction that covers session transcripts (sanitize_output, which masks credential prompts, tokens and keys) is applied bySessionLoggeronly and has never applied totracing, so anyone following the documented advice to answer a password prompt from an expect rule had that password in~/.local/share/rustconn/logsand in the terminal RustConn was started from. Both sites now log the response length instead of its contents. -
"Move to New Tab" in a split pane's context menu did nothing (issue #252) — the handler mutated
SplitLayoutModeldirectly instead of going throughSplitViewAdapter::remove_panel, the only caller ofrebuild_widgets, then recorded the result inlast_drop_outcomeand setneeds_rebuildfor the UI layer to act on. Nothing ever did:take_last_drop_outcomeandcheck_and_rebuildhad no callers outsideadapter.rs. The model therefore drifted out of step with both the widget tree and the bridge'spanel_uuid_map/panesmaps, no tab was created and no widget moved — the item was inert for as long as it existed. "Close Connection" in the same menu had the identical defect. Both now activate the window actions that own the full teardown, and the menu is ordered with the destructive item last per the HIG. -
A collapsed split left its layout behind, and the next split reused it — when a split came down to a single session,
win.close-panehid the bridge widget but left the bridge insession_split_bridges.get_or_create_session_bridgereuses whatever it finds there, so the next split on any of those sessions picked up the hidden, half-wired layout instead of building a fresh one. Every teardown path now drops the entry for each session that took part. -
Broadcast kept mirroring keystrokes out of a session that had left the split — the per-terminal
commithandler is connected once for the life of a session and never disconnected, and it was gated onbridge.broadcast_activealone. A session whose pane was closed, or that moved into a different split, therefore went on feeding its input into the layout it had left. The handler now also checks that the session is still displayed in that bridge. -
SFTP could not reach a host behind a jump host (issue #255) — copying an RDP connection, switching it to SFTP and configuring the same jump host produced an
mcwindow showing local files in both panels. Midnight Commander'ssh://filesystem shells out tosshwith a fixed argument list (ssh -p <port> -l <user> <host> "echo SHELL:; /bin/sh"), and its URI syntax carries nothing but user, host and port — the documented options are compression,rshand a port number. Every SSH setting the connection held was therefore dropped on that path: the bastion most visibly, but also the identity file,HostKeyAliasand custom options. The connection then failed and mc silently left the panel on the local directory, which is what "local files on both sides" was. The one injection point mc leaves open is$PATH, since it invokessshby name: RustConn now writes a per-sessionsshwrapper plus a generatedssh_configunder$XDG_RUNTIME_DIRand prepends that directory to mc'sPATH, so the connection's real settings reach thesshmc spawns. The generated file ends withMatch allfollowed byInclude ~/.ssh/config— without theMatch allthe include would be scoped to the precedingHostblock and silently drop the user's own aliases — and each jump hop gets its own block, becauseProxyJumpdoes not pass-idown to a bastion (issue #241). This generalises the Flatpak-only wrapper that used to inject a writableknown_hostsand nothing else. -
A jump host picked from the connection list was invisible to every SFTP path — a bastion can be configured two ways: as free text in
proxy_jump, or as a reference to another connection (jump_host_id), which is what the jump-host dropdown in the connection editor writes. Resolving the reference form needs the whole connection list, so it only ever happened inside the GUI crate, andrustconn-core— where the SFTP builders live — saw the string form alone. ThesftpCLI consequently built-Jfrom the string only, and thessh … pwdprobe that finds the login home directory (issue #212) could not reach a host behind a picker-selected bastion, so the file browser fell back to the server root. Chain resolution now lives inrustconn-core::connection::jump_chainand is shared by all of them, with the hop ordering, cycle guard and 10-hop cap the SSH terminal path already used. -
rustconn-cli sftprefused connections whose protocol is SFTP — the guard acceptedProtocolType::Sshonly, so the exact connection shape issue #255 describes was rejected with "SFTP is only available for SSH connections". SFTP carries the sameSshConfig; both are now accepted. -
A pre-connect port check ran against hosts reachable only through a hand-typed bastion —
bypasses_direct_probe()recognisedjump_host_idandproxy_commandbut notproxy_jump, so a connection whose bastion was entered as free text still got a direct TCP probe that could only time out before the connection was attempted. -
Auto-login answered password-change prompts with the stored password —
looks_like_password_promptaccepted any line ending inpassword:, soOld Password:,(current) UNIX password:,New password:andRetype new password:all triggered auto-fill. The stored login password is never the right answer to any of these — typing it atNew password:would set the new password to the old one, and repeating it at(current)after a forced change would loop. Lines containingold password,(current),new password,retype password,confirm password,verify passwordorrepeat password(plus Ukrainian and Russian equivalents) are now rejected alongside passphrase prompts. -
A resolved expect-rule password lingered in memory after the rule fired —
prepare_rules_from_configsubstituted${password}into the rule'sresponsefield as a plainString, which was then stored in theExpectEnginefor the life of the session and freed without scrubbing. The response is now copied into aZeroizing<String>before being fed to VTE, and the stored copy is explicitly zeroized before removal.AutomationState::Dropscrubs any rules still holding credentials when the session closes. -
The mc SFTP wrapper accepted a non-executable
sshfrom PATH —find_real_sshchecked.is_file()but not the executable permission, so a broken symlink or a data file namedsshearlier in PATH would be selected and fail. It now also verifiesmode & 0o111 != 0. -
Stale SSH agent key files accumulated after a crash —
materialize_agent_identitywrites a.pubfile under$XDG_RUNTIME_DIR/rustconn/agent-keys/for each agent-sourced connection and removes it on close, but a crash or kill left them behind. A startup prune now removes files older than 12 hours from that directory. -
Broadcast kept a session wired after it left a split, preventing re-wiring in a new one — the per-terminal
commithandler checkedis_session_displayed(which returns false once the session departs), butbroadcast_wired_sessionswas never cleared, sowire_broadcast_for_sessionrefused to re-wire the session when it entered a different split. The set is now cleared on collapse and on individual pane removal.
Improved
- One prompt-watching implementation instead of three — the detect-prompt-and-inject block (a one-shot guard, subscriptions to both
contents-changedandcursor-moved, a 150 ms polling timer scheduled at most once, and a separate 10 s deadline timer) was duplicated verbatim instart_ssh_connection_internalandreconnect_ssh_in_place, about 136 lines each. Both now callwindow::prompt_autofill::install_login_autofill, which owns the username→password state machine, keeps the secret in aSecretStringuntil the moment it is handed to VTE (thenZeroizing), and replaces the two-timer arrangement with one repeating timer that checks its own deadline. The issue #191 bastion guard stays at both call sites: the target password is still only injected when there is no jump host, the bastion was authenticated out-of-band viaSSH_ASKPASS, or it uses key/agent auth and never prompts in the VTE. - Prompt matching moved into
rustconn-core— the newconnection::login_promptmodule holdslooks_like_username_prompt,LoginPromptMatcherand the default prompt list next to the existinglooks_like_password_prompt, so the matching rules are unit-testable without gtk/vte. Password wins when a line matches both, since answering a password prompt with an account name would send the wrong secret. - The file-manager SFTP path falls back to mc when a jump host is configured — the GVFS sftp backend spawns its own
sshwith a hardcoded argument list (-oControlMaster,-oControlPath,-oForwardX11 no,-oNoHostAuthenticationForLocalhost) and accepts no-F, and it is D-Bus-activated so itsPATHis not ours to set; a jump host cannot be applied there at all. Rather than showing a warning and opening a file browser that cannot reach the target, both SFTP paths now detect the jump host and automatically launch mc with the generatedssh_configwrapper instead — which does support bastions (issue #255). A toast informs the user of the fallback. The same warning is logged byrustconn-cli sftp(the CLI always used mc).
Documentation
-
"Variable Substitution in Responses" rewritten around what the placeholders actually do — the four built-ins are listed with where each value comes from, next to the precedence rule against a same-named global, the fact that the response is typed into the session rather than passed to a shell (so metacharacters survive), the line-break rejection, the escape-before-substitute order, and why a repeated prompt is handed back to the user instead of answered twice. The info label in the Automation tab and the group editor is left as it was: it promised these placeholders "resolve at connection time", which the fix above makes true, and rewording it would have retired a msgid that all fourteen catalogues already translate.
-
New "Automatic Login (Telnet & Serial)" section in the user guide — what is typed and where it comes from, substring-not-regex matching, the table of prompts recognized without configuration, the one-shot and 10-second limits, and a reminder that Telnet still sends the password in clear text no matter who types it.
Dependencies
- Updated: ipnet 2.12.0 → 2.12.1, libredox 0.1.18 → 0.1.19. Both are transitive patch releases picked up when the lockfile was regenerated;
cargo deny check advisoriesis clean. Every bundled Flatpak module is already at its latest upstream release (GNOME runtime 50, FreeRDP 3.30.0, VTE 0.80.5, mc 4.8.33, cJSON 1.7.19, openh264 2.6.0, waypipe 0.11.0), and TigerVNC — the only pinned CLI download — is current at 1.16.2.
Known Issues
- Windows Explorer does not auto-refresh after file operations in an RDP drive share — rename, delete and copy operations now work correctly in the embedded RDP drive redirection (issue #256), but Explorer's directory listing does not update until the user presses F5. This happens because
ironrdp-connectordoes not yet expose theClientDriveNotifyChangeDirectoryResponsePDU, so change-notification IRPs are never completed and Explorer keeps its stale view. The underlying data is correct; only the display is stale. A fix requires upstreamironrdpwork and is tracked separately.
Installation
Flatpak (Recommended)
flatpak install flathub io.github.totoshko88.RustConnSnap
sudo snap install rustconnDebian/Ubuntu (.deb from this release)
sudo dpkg -i rustconn_0.19.12_amd64.deb
sudo apt-get install -f # Install dependencies if neededFedora (.rpm from this release)
sudo dnf install rustconn-0.19.12-1.fc44.x86_64.rpmAppImage
chmod +x RustConn-0.19.12-x86_64.AppImage
./RustConn-0.19.12-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