use std::path::Path; use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result, anyhow, bail}; use russh::keys; use russh::keys::PrivateKey; use russh::keys::ssh_key::{self, AuthorizedKeys}; use russh::server::Config as RusshServerConfig; use russh::{MethodKind, MethodSet}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SshAuthMode { Key, Password, Both, } impl SshAuthMode { pub fn parse(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { "key" | "publickey" => Some(Self::Key), "password" => Some(Self::Password), "both" => Some(Self::Both), _ => None, } } pub fn as_str(self) -> &'static str { match self { Self::Key => "key", Self::Password => "password", Self::Both => "both", } } } #[derive(Debug, Clone)] pub struct RtrSshRuntimeConfig { pub server_config: Arc, pub authorized_keys: Arc>, pub username: Arc, pub subsystem_name: Arc, pub password: Option>, } pub fn load_rtr_ssh_runtime_config( host_key_path: impl AsRef, authorized_keys_path: impl AsRef, username: &str, subsystem_name: &str, auth_mode: SshAuthMode, password: Option<&str>, inactivity_timeout: Option, keepalive_interval: Option, ) -> Result { if username.trim().is_empty() { bail!("SSH username must not be empty"); } if subsystem_name.trim().is_empty() { bail!("SSH subsystem name must not be empty"); } let host_key = load_host_key(host_key_path.as_ref())?; let authorized_keys = if matches!(auth_mode, SshAuthMode::Key | SshAuthMode::Both) { load_authorized_keys(authorized_keys_path.as_ref())? } else { Vec::new() }; let password = password.map(str::trim).filter(|value| !value.is_empty()); if matches!(auth_mode, SshAuthMode::Password | SshAuthMode::Both) && password.is_none() { bail!( "SSH auth mode '{}' requires non-empty password", auth_mode.as_str() ); } let mut methods = MethodSet::empty(); if matches!(auth_mode, SshAuthMode::Key | SshAuthMode::Both) { methods.push(MethodKind::PublicKey); } if matches!(auth_mode, SshAuthMode::Password | SshAuthMode::Both) { methods.push(MethodKind::Password); } let server_config = RusshServerConfig { methods, keys: vec![host_key], inactivity_timeout, keepalive_interval, keepalive_max: 3, auth_rejection_time: Duration::from_secs(1), auth_rejection_time_initial: Some(Duration::from_secs(0)), ..Default::default() }; Ok(RtrSshRuntimeConfig { server_config: Arc::new(server_config), authorized_keys: Arc::new(authorized_keys), username: Arc::from(username.trim()), subsystem_name: Arc::from(subsystem_name.trim()), password: password.map(Arc::from), }) } fn load_host_key(path: &Path) -> Result { keys::load_secret_key(path, None).with_context(|| { format!( "failed to load SSH host private key from {} (OpenSSH private key expected)", path.display() ) }) } fn load_authorized_keys(path: &Path) -> Result> { let entries = AuthorizedKeys::read_file(path).with_context(|| { format!( "failed to read SSH authorized_keys file from {}", path.display() ) })?; let mut keys = Vec::with_capacity(entries.len()); for entry in entries { keys.push(entry.public_key().clone()); } if keys.is_empty() { return Err(anyhow!( "SSH authorized_keys file {} does not contain any usable keys", path.display() )); } Ok(keys) }