Skip to content

v0.19.12

Choose a tag to compare

@github-actions github-actions released this 04 Aug 13:09
· 30 commits to main since this release

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: reset only switches back to the normal screen in its clear_history branch, so a session that died inside a full-screen app (vim, htop, less) would have kept showing that app's frozen screen — DECRST 1049 is 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 same reparent_terminal_to_tab path 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 reconnectmax_scrollback_on_reconnect in config.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 — edit config.toml directly.

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_config substituted against a VariableManager seeded exclusively from settings.global_variables, and substitute_for_command replaces 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_command exists 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 a Variablewhen 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, \t and \\ 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 \n was sent as a line break, splitting it in two and submitting the first half; one containing \s lost 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, since tracing output is not redacted.

  • Connect detection and prompt detection read the oldest scrollback instead of the screen (issue #253)vte_terminal_get_cursor_position documents its row as absolute, counted from the start of the scrollback rather than the top of the screen, and both get_terminal_text and the fallback in cursor_line_text addressed rows 0..row_count as 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 textAutomationSession logged 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 by SessionLogger only and has never applied to tracing, so anyone following the documented advice to answer a password prompt from an expect rule had that password in ~/.local/share/rustconn/logs and 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 SplitLayoutModel directly instead of going through SplitViewAdapter::remove_panel, the only caller of rebuild_widgets, then recorded the result in last_drop_outcome and set needs_rebuild for the UI layer to act on. Nothing ever did: take_last_drop_outcome and check_and_rebuild had no callers outside adapter.rs. The model therefore drifted out of step with both the widget tree and the bridge's panel_uuid_map/panes maps, 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-pane hid the bridge widget but left the bridge in session_split_bridges. get_or_create_session_bridge reuses 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 commit handler is connected once for the life of a session and never disconnected, and it was gated on bridge.broadcast_active alone. 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 mc window showing local files in both panels. Midnight Commander's sh:// filesystem shells out to ssh with 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, rsh and a port number. Every SSH setting the connection held was therefore dropped on that path: the bastion most visibly, but also the identity file, HostKeyAlias and 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 invokes ssh by name: RustConn now writes a per-session ssh wrapper plus a generated ssh_config under $XDG_RUNTIME_DIR and prepends that directory to mc's PATH, so the connection's real settings reach the ssh mc spawns. The generated file ends with Match all followed by Include ~/.ssh/config — without the Match all the include would be scoped to the preceding Host block and silently drop the user's own aliases — and each jump hop gets its own block, because ProxyJump does not pass -i down to a bastion (issue #241). This generalises the Flatpak-only wrapper that used to inject a writable known_hosts and 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, and rustconn-core — where the SFTP builders live — saw the string form alone. The sftp CLI consequently built -J from the string only, and the ssh … pwd probe 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 in rustconn-core::connection::jump_chain and is shared by all of them, with the hop ordering, cycle guard and 10-hop cap the SSH terminal path already used.

  • rustconn-cli sftp refused connections whose protocol is SFTP — the guard accepted ProtocolType::Ssh only, so the exact connection shape issue #255 describes was rejected with "SFTP is only available for SSH connections". SFTP carries the same SshConfig; both are now accepted.

  • A pre-connect port check ran against hosts reachable only through a hand-typed bastionbypasses_direct_probe() recognised jump_host_id and proxy_command but not proxy_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 passwordlooks_like_password_prompt accepted any line ending in password:, so Old Password:, (current) UNIX password:, New password: and Retype new password: all triggered auto-fill. The stored login password is never the right answer to any of these — typing it at New password: would set the new password to the old one, and repeating it at (current) after a forced change would loop. Lines containing old password, (current), new password, retype password, confirm password, verify password or repeat password (plus Ukrainian and Russian equivalents) are now rejected alongside passphrase prompts.

  • A resolved expect-rule password lingered in memory after the rule firedprepare_rules_from_config substituted ${password} into the rule's response field as a plain String, which was then stored in the ExpectEngine for the life of the session and freed without scrubbing. The response is now copied into a Zeroizing<String> before being fed to VTE, and the stored copy is explicitly zeroized before removal. AutomationState::Drop scrubs any rules still holding credentials when the session closes.

  • The mc SFTP wrapper accepted a non-executable ssh from PATHfind_real_ssh checked .is_file() but not the executable permission, so a broken symlink or a data file named ssh earlier in PATH would be selected and fail. It now also verifies mode & 0o111 != 0.

  • Stale SSH agent key files accumulated after a crashmaterialize_agent_identity writes a .pub file 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 commit handler checked is_session_displayed (which returns false once the session departs), but broadcast_wired_sessions was never cleared, so wire_broadcast_for_session refused 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-changed and cursor-moved, a 150 ms polling timer scheduled at most once, and a separate 10 s deadline timer) was duplicated verbatim in start_ssh_connection_internal and reconnect_ssh_in_place, about 136 lines each. Both now call window::prompt_autofill::install_login_autofill, which owns the username→password state machine, keeps the secret in a SecretString until the moment it is handed to VTE (then Zeroizing), 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 via SSH_ASKPASS, or it uses key/agent auth and never prompts in the VTE.
  • Prompt matching moved into rustconn-core — the new connection::login_prompt module holds looks_like_username_prompt, LoginPromptMatcher and the default prompt list next to the existing looks_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 ssh with a hardcoded argument list (-oControlMaster, -oControlPath, -oForwardX11 no, -oNoHostAuthenticationForLocalhost) and accepts no -F, and it is D-Bus-activated so its PATH is 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 generated ssh_config wrapper instead — which does support bastions (issue #255). A toast informs the user of the fallback. The same warning is logged by rustconn-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 advisories is 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-connector does not yet expose the ClientDriveNotifyChangeDirectoryResponse PDU, 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 upstream ironrdp work and is tracked separately.

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

Fedora (.rpm from this release)

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

AppImage

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