fix: generate SSH keys in-process, verify loopback tunnel peer, bound SSH command output

- SSH key pairs are generated with ssh-key (Ed25519/ECDSA P-256/RSA) and
  encrypted in-process, so the passphrase no longer appears as an ssh-keygen
  command-line argument visible in the process list.
- open_tunnel's loopback bridge checks the accepted peer is our own client
  socket, so a local process racing for the ephemeral port can't be spliced
  into the IRC/VNC SSH tunnel.
- run_command caps buffered output at 1 MiB; local-forward abort handles are
  pruned as connections finish; SOCKS5 host/credential lengths over 255 bytes
  are rejected instead of being truncated into a malformed request.
This commit is contained in:
Andy Dixon
2026-10-12 07:22:31 +01:00
parent 9cf221779b
commit 0a18f7e578
3 changed files with 165 additions and 42 deletions
+3 -26
View File
@@ -283,7 +283,7 @@ impl Service {
Ok(n)
}
// --- SSH keys (via ssh-keygen, as before) -------------------------------------------
// --- SSH keys -----------------------------------------------------------------------
fn ssh_dir() -> Result<PathBuf> {
let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).ok_or("unable to determine home directory")?;
@@ -310,32 +310,9 @@ impl Service {
Ok(out)
}
/// Generates a key pair in `~/.ssh` in-process (the passphrase never reaches a command line).
pub fn generate_ssh_key(&self, name: &str, kind: &str, bits: u32, passphrase: &str) -> Result<()> {
let name = keyfiles::validate_ssh_key_name(name)?;
let kind = if kind.trim().is_empty() { "ed25519".to_string() } else { kind.trim().to_lowercase() };
let bits = match kind.as_str() {
"ed25519" | "ecdsa" => bits,
"rsa" if bits == 0 => 4096,
"rsa" if [2048, 3072, 4096].contains(&bits) => bits,
"rsa" => return Err(format!("unsupported RSA key size {bits} (use 2048, 3072, or 4096)").into()),
k => return Err(format!("unsupported SSH key type {k:?}").into()),
};
let dir = Self::ssh_dir()?;
std::fs::create_dir_all(&dir)?;
let path = dir.join(&name);
if path.exists() {
return Err(format!("key {name:?} already exists").into());
}
let mut cmd = std::process::Command::new("ssh-keygen");
cmd.args(["-q", "-t", &kind, "-f"]).arg(&path).args(["-C", &format!("{name}@nexus-terminal"), "-N", passphrase]);
if kind == "rsa" {
cmd.args(["-b", &bits.to_string()]);
}
let out = cmd.stdin(std::process::Stdio::null()).output().map_err(|e| format!("ssh-keygen failed: {e}"))?;
if !out.status.success() {
return Err(format!("ssh-keygen failed: {}", String::from_utf8_lossy(&out.stderr).trim()).into());
}
Ok(())
crate::clients::ssh::generate_key_pair(&Self::ssh_dir()?, name, kind, bits, passphrase).map(|_| ())
}
pub fn delete_ssh_key(&self, name: &str) -> Result<()> {