pub mod audit;
pub mod auth;
pub mod config;
pub mod exec;
pub mod filter;
pub mod handler;
pub mod pty;
pub mod scp;
pub mod security;
pub mod session;
pub mod sftp;
pub mod shell;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use russh::server::Server;
use tokio::net::{TcpListener, ToSocketAddrs};
use tokio::sync::RwLock;
use crate::shared::rate_limit::RateLimiter;
pub use self::config::{ServerConfig, ServerConfigBuilder};
pub use self::exec::{CommandExecutor, ExecConfig};
pub use self::handler::SshHandler;
pub use self::pty::{PtyConfig as PtyMasterConfig, PtyMaster};
pub use self::security::{
AccessPolicy, AuthRateLimitConfig, AuthRateLimiter, IpAccessControl, SharedIpAccessControl,
};
pub use self::session::{
ChannelMode, ChannelState, PtyConfig, SessionConfig, SessionError, SessionId, SessionInfo,
SessionManager, SessionStats,
};
pub use self::shell::ShellSession;
pub struct BsshServer {
config: Arc<ServerConfig>,
sessions: Arc<RwLock<SessionManager>>,
}
impl BsshServer {
pub fn new(config: ServerConfig) -> Self {
let session_config = config.session_config();
let sessions = SessionManager::with_config(session_config);
Self {
config: Arc::new(config),
sessions: Arc::new(RwLock::new(sessions)),
}
}
pub fn config(&self) -> &ServerConfig {
&self.config
}
pub fn sessions(&self) -> &Arc<RwLock<SessionManager>> {
&self.sessions
}
pub async fn run(&self) -> Result<()> {
let addr = &self.config.listen_address;
tracing::info!(address = %addr, "Starting SSH server");
let russh_config = self.build_russh_config()?;
self.run_on_address(Arc::new(russh_config), addr).await
}
pub async fn run_at(&self, addr: impl ToSocketAddrs + std::fmt::Debug) -> Result<()> {
tracing::info!(address = ?addr, "Starting SSH server");
let russh_config = self.build_russh_config()?;
self.run_on_address(Arc::new(russh_config), addr).await
}
fn build_russh_config(&self) -> Result<russh::server::Config> {
if !self.config.has_host_keys() {
anyhow::bail!("No host keys configured. At least one host key is required.");
}
let mut keys = Vec::new();
for key_path in &self.config.host_keys {
let key = load_host_key(key_path)?;
keys.push(key);
}
tracing::info!(key_count = keys.len(), "Loaded host keys");
Ok(russh::server::Config {
keys,
auth_rejection_time: Duration::from_secs(3),
auth_rejection_time_initial: Some(Duration::from_secs(0)),
max_auth_attempts: self.config.max_auth_attempts as usize,
inactivity_timeout: self.config.idle_timeout(),
..Default::default()
})
}
async fn run_on_address(
&self,
russh_config: Arc<russh::server::Config>,
addr: impl ToSocketAddrs,
) -> Result<()> {
let socket = TcpListener::bind(addr)
.await
.context("Failed to bind to address")?;
tracing::info!(
local_addr = ?socket.local_addr(),
"SSH server listening"
);
let rate_limiter = RateLimiter::with_simple_config(100, 10.0);
let whitelist_ips: Vec<std::net::IpAddr> = self
.config
.whitelist_ips
.iter()
.filter_map(|s| {
s.parse().map_err(|e| {
tracing::warn!(ip = %s, error = %e, "Invalid whitelist IP address in config, skipping");
e
}).ok()
})
.collect();
let auth_config = AuthRateLimitConfig::new(
self.config.max_auth_attempts,
self.config.auth_window_secs,
self.config.ban_time_secs,
)
.with_whitelist(whitelist_ips);
let auth_rate_limiter = AuthRateLimiter::new(auth_config);
tracing::info!(
max_attempts = self.config.max_auth_attempts,
auth_window_secs = self.config.auth_window_secs,
ban_time_secs = self.config.ban_time_secs,
whitelist_count = self.config.whitelist_ips.len(),
"Auth rate limiter configured"
);
let ip_access_control =
IpAccessControl::from_config(&self.config.allowed_ips, &self.config.blocked_ips)
.context("Failed to configure IP access control")?;
let shared_ip_access = SharedIpAccessControl::new(ip_access_control);
let cleanup_limiter = auth_rate_limiter.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
cleanup_limiter.cleanup().await;
}
});
let mut server = BsshServerRunner {
config: Arc::clone(&self.config),
sessions: Arc::clone(&self.sessions),
rate_limiter,
auth_rate_limiter,
ip_access_control: shared_ip_access,
};
server
.run_on_socket(russh_config, &socket)
.await
.map_err(|e| anyhow::anyhow!("Server error: {}", e))
}
pub async fn session_count(&self) -> usize {
self.sessions.read().await.session_count()
}
pub async fn is_at_capacity(&self) -> bool {
self.sessions.read().await.is_at_capacity()
}
}
#[derive(Clone)]
struct BsshServerRunner {
config: Arc<ServerConfig>,
sessions: Arc<RwLock<SessionManager>>,
rate_limiter: RateLimiter<String>,
auth_rate_limiter: AuthRateLimiter,
ip_access_control: SharedIpAccessControl,
}
impl russh::server::Server for BsshServerRunner {
type Handler = SshHandler;
fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler {
if let Some(addr) = peer_addr {
let ip = addr.ip();
if self.ip_access_control.check_sync(&ip) == AccessPolicy::Deny {
tracing::info!(
ip = %ip,
"Connection rejected by IP access control"
);
return SshHandler::rejected(
peer_addr,
Arc::clone(&self.config),
Arc::clone(&self.sessions),
);
}
if let Ok(is_banned) = tokio::runtime::Handle::try_current()
.map(|h| h.block_on(self.auth_rate_limiter.is_banned(&ip)))
{
if is_banned {
tracing::info!(
ip = %ip,
"Connection rejected from banned IP"
);
return SshHandler::rejected(
peer_addr,
Arc::clone(&self.config),
Arc::clone(&self.sessions),
);
}
}
}
tracing::info!(
peer = ?peer_addr,
"New client connection"
);
SshHandler::with_rate_limiters(
peer_addr,
Arc::clone(&self.config),
Arc::clone(&self.sessions),
self.rate_limiter.clone(),
self.auth_rate_limiter.clone(),
)
}
fn handle_session_error(&mut self, error: <Self::Handler as russh::server::Handler>::Error) {
tracing::error!(
error = %error,
"Session error"
);
}
}
fn load_host_key(path: impl AsRef<Path>) -> Result<russh::keys::PrivateKey> {
let path = path.as_ref();
tracing::debug!(path = %path.display(), "Loading host key");
russh::keys::PrivateKey::read_openssh_file(path)
.with_context(|| format!("Failed to load host key from {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_creation() {
let config = ServerConfig::builder()
.listen_address("127.0.0.1:2222")
.max_connections(50)
.build();
let server = BsshServer::new(config);
assert_eq!(server.config().listen_address, "127.0.0.1:2222");
assert_eq!(server.config().max_connections, 50);
}
#[test]
fn test_build_russh_config_no_keys() {
let config = ServerConfig::builder().build();
let server = BsshServer::new(config);
let result = server.build_russh_config();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No host keys"));
}
#[tokio::test]
async fn test_session_count() {
let config = ServerConfig::builder().host_key("/nonexistent/key").build();
let server = BsshServer::new(config);
assert_eq!(server.session_count().await, 0);
assert!(!server.is_at_capacity().await);
}
#[tokio::test]
async fn test_session_manager_access() {
let config = ServerConfig::builder()
.max_connections(10)
.host_key("/nonexistent/key")
.build();
let server = BsshServer::new(config);
{
let mut sessions = server.sessions().write().await;
let info = sessions.create_session(None);
assert!(info.is_some());
}
assert_eq!(server.session_count().await, 1);
}
}