137 lines
3.9 KiB
Rust
137 lines
3.9 KiB
Rust
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<Self> {
|
|
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<RusshServerConfig>,
|
|
pub authorized_keys: Arc<Vec<ssh_key::PublicKey>>,
|
|
pub username: Arc<str>,
|
|
pub subsystem_name: Arc<str>,
|
|
pub password: Option<Arc<str>>,
|
|
}
|
|
|
|
pub fn load_rtr_ssh_runtime_config(
|
|
host_key_path: impl AsRef<Path>,
|
|
authorized_keys_path: impl AsRef<Path>,
|
|
username: &str,
|
|
subsystem_name: &str,
|
|
auth_mode: SshAuthMode,
|
|
password: Option<&str>,
|
|
inactivity_timeout: Option<Duration>,
|
|
keepalive_interval: Option<Duration>,
|
|
) -> Result<RtrSshRuntimeConfig> {
|
|
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<PrivateKey> {
|
|
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<Vec<ssh_key::PublicKey>> {
|
|
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)
|
|
}
|