From 987b5c7132e0a30a57c09f5fe8a90bb84b120b6c Mon Sep 17 00:00:00 2001 From: "n.shafiev" Date: Wed, 28 Oct 2026 20:07:40 +0400 Subject: [PATCH] feat(rustconn-core): add quicssh proxy preset per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework PR #396 toward the preset approach: a QUIC button on the editor's ProxyCommand row fills QUIC_SSH_PROXY_COMMAND (quicssh client --addr %h:%p) instead of introducing a ProtocolType::QuicSsh variant, so the connection stays a plain SSH connection — jump hosts, exports, vault keys, connection testing and reconnect apply unchanged, and no new host-to-shell path exists for imported connections (CVE-2023-51385 shape on OpenSSH < 9.6). The password auto-fill relay allow-list recognizes the quicssh command, and detect_quicssh() next to detect_mosh() reports install state plus the Flatpak/Snap sandbox reachability note in the row tooltip. --- CHANGELOG.md | 5 + po/rustconn.pot | 130 +++++++++++++----------- rustconn-core/src/protocol/detection.rs | 16 +++ rustconn-core/src/protocol/mod.rs | 9 +- rustconn-core/src/ssh_tunnel.rs | 16 +++ rustconn/src/dialogs/connection/ssh.rs | 47 ++++++++- 6 files changed, 160 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe27bd8..69802673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to RustConn will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **QUIC (quicssh) preset on the SSH ProxyCommand row — SSH over QUIC via [moul/quicssh](https://github.com/moul/quicssh)** — the editor's ProxyCommand row gets a QUIC button that fills in `quicssh client --addr %h:%p`: an ordinary SSH connection whose transport is the `quicssh client` QUIC tunnel, which keeps connection migration (roaming between networks) and UDP NAT traversal for ISP-NATed hosts. Because it is a preset and not a new protocol type, everything SSH already does applies unchanged — jump hosts, exports, vault keys, connection testing, reconnect. The tooltip reports whether the `quicssh` binary was found (with an install hint and the note that the Flatpak/Snap builds only see binaries reachable inside the sandbox), and the password auto-fill allow-list recognizes the relay, so stored passwords still auto-fill past it. (`rustconn/src/dialogs/connection/ssh.rs`, `rustconn-core/src/ssh_tunnel.rs`, `rustconn-core/src/protocol/detection.rs`) + ## [0.24.0] - 2026-10-28 ### Fixed diff --git a/po/rustconn.pot b/po/rustconn.pot index 06f39bb1..201caaf2 100644 --- a/po/rustconn.pot +++ b/po/rustconn.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: rustconn 0.24.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-28 02:42+0200\n" +"POT-Creation-Date: 2026-10-28 19:57+0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -2650,7 +2650,7 @@ msgid "" msgstr "" #: rustconn/src/dialogs/connection/rdp.rs:466 -#: rustconn/src/dialogs/connection/ssh.rs:805 +#: rustconn/src/dialogs/connection/ssh.rs:850 #: rustconn/src/dialogs/connection/vnc.rs:299 msgid "Multipath TCP" msgstr "" @@ -3489,262 +3489,276 @@ msgstr "" msgid "Custom proxy command (e.g., for .onion hosts)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:701 +#: rustconn/src/dialogs/connection/ssh.rs:702 #: rustconn/src/dialogs/template.rs:706 msgid "Use Only Specified Key" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:702 +#: rustconn/src/dialogs/connection/ssh.rs:703 #: rustconn/src/dialogs/template.rs:707 msgid "Prevents trying other keys (IdentitiesOnly)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:708 +#: rustconn/src/dialogs/connection/ssh.rs:709 #: rustconn/src/dialogs/template.rs:713 msgid "Connection Multiplexing" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:709 +#: rustconn/src/dialogs/connection/ssh.rs:710 #: rustconn/src/dialogs/template.rs:714 msgid "Reuse connections (ControlMaster)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:717 +#: rustconn/src/dialogs/connection/ssh.rs:718 msgid "Keep-Alive Interval" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:718 +#: rustconn/src/dialogs/connection/ssh.rs:719 msgid "Seconds between keep-alive packets (0 = disabled)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:725 +#: rustconn/src/dialogs/connection/ssh.rs:726 msgid "Keep-Alive Count" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:726 +#: rustconn/src/dialogs/connection/ssh.rs:727 msgid "Disconnect after this many unanswered packets" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:759 +#: rustconn/src/dialogs/connection/ssh.rs:761 +msgid "QUIC (quicssh) preset: fills the ProxyCommand with “{}”" +msgstr "" + +#: rustconn/src/dialogs/connection/ssh.rs:767 +msgid "" +"quicssh not found — {}. In the Flatpak/Snap builds it must be reachable " +"inside the sandbox." +msgstr "" + +#: rustconn/src/dialogs/connection/ssh.rs:778 +msgid "Fill ProxyCommand with the QUIC (quicssh) preset" +msgstr "" + +#: rustconn/src/dialogs/connection/ssh.rs:804 #: rustconn/src/dialogs/template.rs:722 msgid "Session" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:764 +#: rustconn/src/dialogs/connection/ssh.rs:809 #: rustconn/src/dialogs/template.rs:727 msgid "Agent Forwarding" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:765 +#: rustconn/src/dialogs/connection/ssh.rs:810 #: rustconn/src/dialogs/template.rs:728 msgid "Forward SSH agent to remote host (-A)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:770 +#: rustconn/src/dialogs/connection/ssh.rs:815 msgid "Waypipe" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:771 +#: rustconn/src/dialogs/connection/ssh.rs:816 msgid "Wayland application forwarding via waypipe" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:776 +#: rustconn/src/dialogs/connection/ssh.rs:821 msgid "X11 Forwarding" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:777 +#: rustconn/src/dialogs/connection/ssh.rs:822 msgid "Forward X11 display to local host (-X)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:782 +#: rustconn/src/dialogs/connection/ssh.rs:827 #: rustconn/src/dialogs/connection/vnc.rs:197 msgid "Compression" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:783 +#: rustconn/src/dialogs/connection/ssh.rs:828 msgid "Enable compression for slow connections (-C)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:788 +#: rustconn/src/dialogs/connection/ssh.rs:833 msgid "Verbose" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:789 +#: rustconn/src/dialogs/connection/ssh.rs:834 msgid "Show SSH debug output for troubleshooting (-v)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:796 +#: rustconn/src/dialogs/connection/ssh.rs:841 msgid "Use multiple network paths for mobility and aggregation (mptcpize run)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:799 +#: rustconn/src/dialogs/connection/ssh.rs:844 msgid "" "Use multiple network paths for mobility and aggregation. Install mptcpize " "(mptcpd) to enable" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:803 +#: rustconn/src/dialogs/connection/ssh.rs:848 msgid "" "Use multiple network paths (mptcpize run). Not available: kernel MPTCP is " "disabled" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:811 +#: rustconn/src/dialogs/connection/ssh.rs:856 #: rustconn/src/dialogs/template.rs:740 msgid "Startup Command" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:812 +#: rustconn/src/dialogs/connection/ssh.rs:857 #: rustconn/src/dialogs/template.rs:741 msgid "Execute after connection established" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:813 +#: rustconn/src/dialogs/connection/ssh.rs:858 #: rustconn/src/dialogs/template.rs:735 msgid "Command to run on connect" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:818 +#: rustconn/src/dialogs/connection/ssh.rs:863 #: rustconn/src/dialogs/template.rs:754 msgid "Custom Options" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:819 +#: rustconn/src/dialogs/connection/ssh.rs:864 msgid "Key=Value, comma-separated (-o flags)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:826 +#: rustconn/src/dialogs/connection/ssh.rs:871 #: rustconn/src/window/edit_group.rs:853 msgid "SSH Agent Socket" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:829 +#: rustconn/src/dialogs/connection/ssh.rs:874 msgid "Overrides global setting and auto-detected socket for this connection" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:856 +#: rustconn/src/dialogs/connection/ssh.rs:901 msgid "PKCS#11 Provider" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:859 +#: rustconn/src/dialogs/connection/ssh.rs:904 msgid "" "Path to a PKCS#11 library for hardware-token login (e.g. /usr/lib64/" "libykcs11.so.2)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:885 +#: rustconn/src/dialogs/connection/ssh.rs:930 msgid "SFTP Remote Directory" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:888 +#: rustconn/src/dialogs/connection/ssh.rs:933 msgid "" "Directory the SFTP file browser opens in. Leave empty to detect the home " "directory automatically." msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:917 +#: rustconn/src/dialogs/connection/ssh.rs:962 #: rustconn/src/dialogs/connection/telnet.rs:52 msgid "Keyboard" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:919 +#: rustconn/src/dialogs/connection/ssh.rs:964 #: rustconn/src/dialogs/connection/telnet.rs:54 msgid "" "If Backspace or Delete is not recognised, try Backspace (^H) or Delete (^?) " "— devices that reject one default usually reject both" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:930 +#: rustconn/src/dialogs/connection/ssh.rs:975 #: rustconn/src/dialogs/connection/telnet.rs:74 msgid "Backspace sends" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:931 +#: rustconn/src/dialogs/connection/ssh.rs:976 #: rustconn/src/dialogs/connection/telnet.rs:75 msgid "What the Backspace key sends to the remote" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:943 +#: rustconn/src/dialogs/connection/ssh.rs:988 #: rustconn/src/dialogs/connection/telnet.rs:93 msgid "Delete sends" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:944 +#: rustconn/src/dialogs/connection/ssh.rs:989 #: rustconn/src/dialogs/connection/telnet.rs:94 msgid "What the Delete key sends to the remote" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:976 +#: rustconn/src/dialogs/connection/ssh.rs:1021 msgid "Port Forwarding" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:977 +#: rustconn/src/dialogs/connection/ssh.rs:1022 msgid "Forward ports through the SSH tunnel" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:982 +#: rustconn/src/dialogs/connection/ssh.rs:1027 #: rustconn/src/dialogs/tunnel_builder/step_forwards.rs:420 msgid "Local (-L)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:983 +#: rustconn/src/dialogs/connection/ssh.rs:1028 #: rustconn/src/dialogs/tunnel_builder/step_forwards.rs:421 msgid "Remote (-R)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:984 +#: rustconn/src/dialogs/connection/ssh.rs:1029 #: rustconn/src/dialogs/tunnel_builder/step_forwards.rs:422 msgid "Dynamic (-D)" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:992 +#: rustconn/src/dialogs/connection/ssh.rs:1037 msgid "Direction" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:993 +#: rustconn/src/dialogs/connection/ssh.rs:1038 msgid "Type of port forwarding" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1006 +#: rustconn/src/dialogs/connection/ssh.rs:1051 #: rustconn/src/dialogs/tunnel_builder/step_forwards.rs:459 msgid "Local Port" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1007 +#: rustconn/src/dialogs/connection/ssh.rs:1052 msgid "Port to bind locally" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1018 +#: rustconn/src/dialogs/connection/ssh.rs:1063 #: rustconn/src/dialogs/tunnel_builder/step_forwards.rs:477 msgid "Remote Host" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1019 +#: rustconn/src/dialogs/connection/ssh.rs:1064 msgid "Destination host on the remote side" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1032 +#: rustconn/src/dialogs/connection/ssh.rs:1077 #: rustconn/src/dialogs/tunnel_builder/step_forwards.rs:493 msgid "Remote Port" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1033 +#: rustconn/src/dialogs/connection/ssh.rs:1078 msgid "Destination port on the remote side" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1049 +#: rustconn/src/dialogs/connection/ssh.rs:1094 msgid "Add Forward" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1079 +#: rustconn/src/dialogs/connection/ssh.rs:1124 msgid "Port already in use" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1156 +#: rustconn/src/dialogs/connection/ssh.rs:1201 msgid "Remove this port forward" msgstr "" -#: rustconn/src/dialogs/connection/ssh.rs:1159 +#: rustconn/src/dialogs/connection/ssh.rs:1204 msgid "Remove port forward" msgstr "" diff --git a/rustconn-core/src/protocol/detection.rs b/rustconn-core/src/protocol/detection.rs index 323c91a6..ed3c3e69 100644 --- a/rustconn-core/src/protocol/detection.rs +++ b/rustconn-core/src/protocol/detection.rs @@ -670,6 +670,22 @@ pub fn detect_mosh() -> ClientInfo { ClientInfo::not_installed("mosh", "Install mosh package") } +/// Detects the quicssh client ([moul/quicssh](https://github.com/moul/quicssh), +/// SSH over QUIC) +/// +/// quicssh is not bundled: it must be installed by the user and, in the +/// Flatpak and Snap builds, reachable from inside the sandbox. +#[must_use] +pub fn detect_quicssh() -> ClientInfo { + if let Some(info) = try_detect_client("quicssh", "quicssh", &["--version"]) { + return info; + } + ClientInfo::not_installed( + "quicssh", + "Install quicssh (github.com/moul/quicssh) and make sure it is on PATH", + ) +} + /// Detects waypipe (Wayland application forwarding proxy) /// /// Waypipe forwards Wayland clients over SSH, similar to `ssh -X` for X11. diff --git a/rustconn-core/src/protocol/mod.rs b/rustconn-core/src/protocol/mod.rs index 54e73577..52d1821c 100644 --- a/rustconn-core/src/protocol/mod.rs +++ b/rustconn-core/src/protocol/mod.rs @@ -29,10 +29,11 @@ pub use detection::{ ClientDetectionResult, ClientInfo, FREERDP_WAYLAND_FIRST, FREERDP_X11_FIRST, ZeroTrustDetectionResult, available_vnc_viewers, detect_aws_cli, detect_azure_cli, detect_boundary, detect_cloudflared, detect_gcloud_cli, detect_hoop, detect_kubectl, - detect_mosh, detect_oci_cli, detect_picocom, detect_rdp_client, detect_spice_client, - detect_ssh_client, detect_tailscale, detect_teleport, detect_telnet_client, detect_vnc_client, - detect_vnc_viewer_name, detect_vnc_viewer_path, detect_waypipe, freerdp_launch_order, - is_wayland_session, resolve_freerdp_client, resolve_vnc_viewer, + detect_mosh, detect_oci_cli, detect_picocom, detect_quicssh, detect_rdp_client, + detect_spice_client, detect_ssh_client, detect_tailscale, detect_teleport, + detect_telnet_client, detect_vnc_client, detect_vnc_viewer_name, detect_vnc_viewer_path, + detect_waypipe, freerdp_launch_order, is_wayland_session, resolve_freerdp_client, + resolve_vnc_viewer, }; pub use freerdp::{ FreeRdpConfig, FreeRdpSizing, build_freerdp_args, contains_freerdp_secret_field, diff --git a/rustconn-core/src/ssh_tunnel.rs b/rustconn-core/src/ssh_tunnel.rs index 2e622f77..8f01a085 100644 --- a/rustconn-core/src/ssh_tunnel.rs +++ b/rustconn-core/src/ssh_tunnel.rs @@ -873,6 +873,15 @@ pub fn probe_tunnel_remote( } } +/// `ProxyCommand` of the [moul/quicssh](https://github.com/moul/quicssh) +/// tunnel (SSH over QUIC), offered as a preset on the editor's ProxyCommand +/// row. +/// +/// `%h` and `%p` are expanded by OpenSSH itself; the command is a pure byte +/// relay, so `proxy_command_can_prompt_for_password` keeps target password +/// auto-fill enabled for it. +pub const QUIC_SSH_PROXY_COMMAND: &str = "quicssh client --addr %h:%p"; + /// Parses a jump host string in `[user@]host[:port]` format and appends /// the correct SSH arguments for use inside a `ProxyCommand`. /// @@ -1044,6 +1053,7 @@ pub fn proxy_command_can_prompt_for_password(proxy_command: &str) -> bool { "cloudflared", "boringproxy", "gost", + "quicssh", ]; // A nested `ssh` invocation is a bastion hop that can prompt — always @@ -1621,6 +1631,12 @@ mod tests { )); // Absolute path to the relay binary is stripped to its basename. assert!(!proxy_command_can_prompt_for_password("/usr/bin/nc %h %p")); + // The quicssh preset (SSH over QUIC, moul/quicssh) is a pure byte + // relay: its server has no auth of its own, so target auto-fill + // stays enabled for connections using the preset. + assert!(!proxy_command_can_prompt_for_password( + "quicssh client --addr %h:%p" + )); } #[test] diff --git a/rustconn/src/dialogs/connection/ssh.rs b/rustconn/src/dialogs/connection/ssh.rs index f9dc19f6..506104db 100644 --- a/rustconn/src/dialogs/connection/ssh.rs +++ b/rustconn/src/dialogs/connection/ssh.rs @@ -19,7 +19,7 @@ use rustconn_core::sftp::{SocketPathValidation, validate_socket_path}; use super::protocol_layout::ProtocolLayoutBuilder; use super::widgets::{CheckboxRowBuilder, EntryRowBuilder}; -use crate::i18n::i18n; +use crate::i18n::{i18n, i18n_f}; /// Named struct for SSH options widgets. /// @@ -694,6 +694,7 @@ fn create_connection_group() -> ConnectionGroupWidgets { .subtitle(i18n("Custom proxy command (e.g., for .onion hosts)")) .placeholder("ncat --proxy 127.0.0.1:9050 --proxy-type socks5 %h %p") .build(); + proxy_command_row.add_suffix(&quicssh_preset_button(&proxy_command_entry)); connection_group.add(&proxy_command_row); // IdentitiesOnly switch @@ -740,6 +741,50 @@ fn create_connection_group() -> ConnectionGroupWidgets { } } +/// The QUIC (quicssh) preset button on the ProxyCommand row. +/// +/// quicssh ([moul/quicssh](https://github.com/moul/quicssh)) is SSH over QUIC: +/// an ordinary `ssh` whose transport is the `quicssh client` stdio proxy. As a +/// preset it only fills the field — the connection stays a plain SSH +/// connection with a user-visible ProxyCommand, so jump hosts, exports and the +/// password auto-fill logic all treat it exactly like any other relay. +/// +/// The tooltip reports whether the `quicssh` binary was found, and notes that +/// the Flatpak/Snap builds only see binaries reachable inside the sandbox. +fn quicssh_preset_button(entry: &Entry) -> Button { + let button = Button::from_icon_name("network-transmit-receive-symbolic"); + button.add_css_class("flat"); + button.set_valign(gtk4::Align::Center); + + let quicssh = rustconn_core::protocol::detect_quicssh(); + let mut tooltip = i18n_f( + "QUIC (quicssh) preset: fills the ProxyCommand with “{}”", + &[rustconn_core::ssh_tunnel::QUIC_SSH_PROXY_COMMAND], + ); + if !quicssh.installed { + tooltip.push(' '); + tooltip.push_str(&i18n_f( + "quicssh not found — {}. In the Flatpak/Snap builds it must be reachable inside the sandbox.", + &[&i18n( + quicssh + .install_hint + .as_deref() + .unwrap_or("Install quicssh (github.com/moul/quicssh)"), + )], + )); + } + button.set_tooltip_text(Some(&tooltip)); + button.update_property(&[gtk4::accessible::Property::Label(&i18n( + "Fill ProxyCommand with the QUIC (quicssh) preset", + ))]); + + let entry = entry.clone(); + button.connect_clicked(move |_| { + entry.set_text(rustconn_core::ssh_tunnel::QUIC_SSH_PROXY_COMMAND); + }); + button +} + /// Creates the Session preferences group fn create_session_group() -> ( adw::PreferencesGroup,