Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Versions follow [Semantic Versioning](https://semver.org/).
## Unreleased

### Bug Fixes
- **Security: a server that offers a host key of another type than the one saved for it is refused.** OmnySSH compared the offered key only with saved keys of the same type. A host saved with its Ed25519 key that suddenly offered only an RSA or ECDSA key was therefore taken for a new host: the key was saved and the login went ahead, password included, and the dashboard did this in the background without anyone connecting. Someone intercepting the connection could get past the saved key this way. Like `ssh`, OmnySSH now refuses any key that matches none of those saved for the host, whatever its type, and says which type is saved. A server that really dropped the type of key it was saved with needs its old key removed once, as `ssh` also asks.
- **A changed host key names its type and, on Windows, whose list held the old one.** OmnySSH 1.1.3 and earlier kept host keys in `%USERPROFILE%\ssh\known_hosts`, a list that PuTTY and OpenSSH never read. When a server was reinstalled, or a new one took over its address, OmnySSH refused it while PuTTY and PowerShell's `ssh` connected, and nothing said why they disagree. A refusal from that file now says it is the list OmnySSH kept up to 1.1.3. Every refusal also names the type of the key the server offered, ED25519, ECDSA or RSA, and the command that prints that key's fingerprint on the server: a server has one key of each type, and comparing the wrong one shows a mismatch that is not there.
- **Servers from the RHEL/CentOS 6 era connect.** OpenSSH before 5.7 failed with "no common key exchange method". Its only key exchange without SHA-1 is diffie-hellman-group-exchange-sha256, which OmnySSH did not offer. Its only host keys are ssh-rsa and ssh-dss, and it predates the rsa-sha2 signatures, so an RSA key could not log in either. OmnySSH now offers diffie-hellman-group-exchange-sha256 and the ssh-rsa host key after every other method, so a server that supports anything newer negotiates exactly as before. An RSA key signs with SHA-1 only for a server that accepts nothing else: one that lists only ssh-rsa as the signatures it takes, or whose version, OpenSSH before 7.2 or Dropbear before 2020.79, predates that list, as PuTTY and `ssh` before 8.8 sign for it. SHA-1 key exchange, CBC ciphers and DSA keys are still left out.
- **FreeBSD hosts show CPU, RAM, load average and top processes.** A FreeBSD server's card showed only disk and uptime: its `top` rejects the options OmnySSH passed, it has neither `free` nor `/proc`, and its `ps` read the list of columns as a single column. CPU is now measured from the kernel's tick counters over one second, so each update of a FreeBSD host takes a second longer. RAM is read from the kernel's page counts, with free and inactive memory, the buffer cache and the ZFS ARC above its minimum counted as available, as htop counts them, so a server on ZFS does not look nearly full. The kernel's idle process, which `ps` charges with all idle time, is left out of the top processes. Where `/proc/loadavg` is missing, the load average now comes from `uptime`, which also fills it in for macOS hosts in the terminal app's host details. As on any system, an account whose login shell is csh or tcsh still shows no metrics.
- **Servers that take only NIST key exchange or aes128-gcm connect, such as Cisco RoomOS video devices.** A server whose key exchange methods include no curve25519 or Diffie-Hellman group 14/16, or whose only cipher is aes128-gcm, failed with "No common algorithm". OmnySSH now also offers ecdh-sha2-nistp256, -nistp384 and -nistp521 key exchange and the aes128-gcm@openssh.com cipher, which macOS's own `ssh` tries first. A host whose only host key is ECDSA P-384, as on a RoomOS device set to ECDSA, now connects the first time too; before, that key was only accepted once it was saved in `known_hosts`. The new key exchange methods and host key type come after the ones offered before, so a server that already worked keeps its key exchange and host key. This needed a newer release of the SSH library (russh 0.63), and building from source with `cargo install omnyssh` now needs Rust 1.89 or later.
- **A server with no algorithm in common says which kind is missing and what it offers.** The error read "No common algorithm", and a key exchange mismatch even "Unknown algorithm", without saying whether the key exchange, host key, cipher or MAC was the problem, so there was nothing to go on. It now reads, for example, "no common cipher; the server offers aes128-cbc". Servers that offer only methods OmnySSH leaves out on purpose, such as SHA-1 Diffie-Hellman or CBC ciphers, still do not connect.
- **macOS: a host on your local network that fails with "No route to host" says macOS may be blocking it.** macOS keeps an app away from devices on the local network until you allow it, and a blocked connection fails at once with "No route to host (os error 65)", while the router usually stays reachable because it is also your DNS server, so it looked like a fault on that one machine. The error now adds that macOS may be blocking the app and where to allow it: System Settings, Privacy & Security, Local Network, then reopen the app. The desktop app also now carries the explanation macOS shows when it asks for that permission. The terminal app needs the permission for the terminal it runs in; Apple's Terminal has it already.
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/omnyssh-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ categories = ["network-programming"]
[dependencies]
# `net`: the local listeners of port forwarding.
tokio = { workspace = true, features = ["net"] }
walkdir = "2"

# SSH. The ring backend, not aws-lc-rs: aws-lc-sys needs cmake and NASM on
# Windows. flate2 keeps zlib compression.
Expand All @@ -31,7 +32,7 @@ thiserror = { workspace = true }

# Async traits used as trait objects: password prompts, service providers
async-trait = "0.1"

async-recursion = "1.1"
# Feature B: Auto SSH Key Setup — timestamp generation
chrono = { workspace = true }

Expand Down
34 changes: 34 additions & 0 deletions crates/omnyssh-core/src/config/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,30 @@ pub struct AppConfig {
pub struct GeneralConfig {
/// Seconds between automatic metric refreshes.
pub refresh_interval: u64,

pub default_shell: String,

/// Path to the system SSH binary.
pub ssh_command: String,

pub max_concurrent_connections: usize,

/// External editor command.
/// Examples:
/// "zed"
/// "code --wait"
/// "vim"
/// Empty = use $VISUAL → $EDITOR → vim
pub external_editor: String,

/// Files larger than this open externally.
pub large_file_mb: u64,

/// Ask before downloading a large remote file.
pub prompt_large_files: bool,

/// Upload remote temp files automatically after editing.
pub auto_upload_external: bool,
}

impl Default for GeneralConfig {
Expand All @@ -32,6 +52,11 @@ impl Default for GeneralConfig {
default_shell: String::from("/bin/bash"),
ssh_command: String::from("ssh"),
max_concurrent_connections: 10,

external_editor: String::new(),
large_file_mb: 5,
prompt_large_files: true,
auto_upload_external: true,
}
}
}
Expand Down Expand Up @@ -261,6 +286,15 @@ pub fn save_theme_to_config(theme_name: &str) -> anyhow::Result<()> {
persist_config(|config| config.ui.theme = theme_name.to_string())
}

/// Saves the entire `[general]` section to the config file.
/// Shared by both the TUI and GUI so editor preferences stay synchronized.
/// # Errors
/// Returns an error if the config file cannot be written or parsed.
pub fn save_general_config(general: &GeneralConfig) -> anyhow::Result<()> {
let general = general.clone();
persist_config(move |config| config.general = general)
}

/// Saves the update-checker preferences to the config file's `[update]`
/// section.
///
Expand Down
33 changes: 31 additions & 2 deletions crates/omnyssh-core/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,30 @@ impl Default for Metrics {
/// Domain events produced by the SSH engine, config loaders, and the update
/// checker. Background tasks send these over a dedicated channel; the
/// frontend wraps them into its own event stream.
#[derive(Debug, Clone, Copy)]
pub enum TransferStage {
Preparing,
Transferring,
}

#[derive(Debug)]
pub enum CoreEvent {
/// SSH metrics received from a background task.
MetricsUpdate(HostId, Metrics),
/// Connection status changed for a host (reported by metrics poller).
HostStatusChanged(HostId, ConnectionStatus),
/// File transfer progress: (transfer_id, bytes_done, bytes_total).
FileTransferProgress(TransferId, u64, u64),
/// File-operation progress: transfers and recursive deletes.
FileTransferProgress {
transfer_id: TransferId,
stage: TransferStage,
root_name: String,
current_file: String,
bytes_done: u64,
bytes_total: u64,
files_done: u32,
files_total: u32,
},

/// An error message surfaced to the user.
Error(String),
/// Host list loaded from disk / SSH config in a background task.
Expand Down Expand Up @@ -110,6 +126,19 @@ pub enum CoreEvent {
SftpDisconnected { reason: String },
/// Preview bytes available for a file.
FilePreviewReady { path: String, content: String },
/// Full remote file body for the in-app editor (size-capped in core).
FileContentReady { path: String, content: String },
/// In-app editor save finished.
FileWriteDone {
path: String,
result: Result<(), String>,
},
/// External editor process finished; frontend uploads if the temp file changed.
ExternalEditDone {
remote_path: String,
local_path: String,
result: Result<(), String>,
},
/// A mutating SFTP operation (delete, mkdir, rename, upload, download) finished.
SftpOpDone { result: Result<(), String> },

Expand Down
Loading