use std::net::SocketAddr; use std::sync::Arc; use russh::keys::PrivateKey; use russh::server::Server as _; use russh::{MethodKind, MethodSet, SshId, server}; use tokio::sync::Semaphore; use config::git::GitSshConfig; use crate::ssh::error::{SshError, SshResult}; use crate::ssh::handler::GitSshHandler; use crate::ssh::service::GitSshService; pub struct GitSshServer { service: Arc, session_slots: Arc, } impl GitSshServer { pub fn new(service: Arc) -> Self { let max = service.config.max_sessions.max(1); Self { service, session_slots: Arc::new(Semaphore::new(max)), } } } impl server::Server for GitSshServer { type Handler = GitSshHandler; fn new_client(&mut self, peer_addr: Option) -> Self::Handler { let permit = self.session_slots.clone().try_acquire_owned().ok(); if permit.is_none() { tracing::warn!(?peer_addr, "ssh session limit reached; rejecting client"); } else { tracing::info!(?peer_addr, "new ssh client connected"); } GitSshHandler::new(self.service.clone(), peer_addr, permit) } fn handle_session_error(&mut self, error: russh::Error) { tracing::debug!("ssh session error: {error}"); } } pub fn load_host_key(path: &str) -> SshResult { let data = std::fs::read_to_string(path) .map_err(|e| SshError::KeyLoadFailed(format!("cannot read {path}: {e}")))?; let key = PrivateKey::from_openssh(&data) .map_err(|e| SshError::KeyLoadFailed(format!("openssh decode failed for {path}: {e}")))?; tracing::debug!(path, "ssh host key loaded"); Ok(key) } pub fn build_server_config(config: &GitSshConfig) -> SshResult { let host_key = load_host_key(&config.host_key_path)?; let mut methods = MethodSet::empty(); methods.push(MethodKind::PublicKey); Ok(server::Config { server_id: SshId::Standard(format!("SSH-2.0-gitdata-{}", env!("CARGO_PKG_VERSION")).into()), methods, keys: vec![host_key], max_auth_attempts: 10, auth_rejection_time: config.auth_rejection_time, inactivity_timeout: config.inactivity_timeout, keepalive_interval: Some(config.keepalive_interval), keepalive_max: config.keepalive_max as usize, ..Default::default() }) } pub async fn run_ssh(service: GitSshService) -> SshResult<()> { let bind_addr = service.config.bind_addr.clone(); let port = service.config.port; let max_sessions = service.config.max_sessions; let config = Arc::new(build_server_config(&service.config)?); let mut server = GitSshServer::new(Arc::new(service)); tracing::info!(%bind_addr, port, max_sessions, "starting git ssh server"); server .run_on_address(config, format!("{bind_addr}:{port}")) .await .map_err(SshError::Io)?; tracing::info!(%bind_addr, port, "git ssh server stopped"); Ok(()) } pub async fn serve_on_listener( service: GitSshService, listener: tokio::net::TcpListener, ) -> SshResult<()> { let config = Arc::new(build_server_config(&service.config)?); let mut server = GitSshServer::new(Arc::new(service)); tracing::info!(local_addr = ?listener.local_addr().ok(), "serving git ssh on listener"); let running = server.run_on_socket(config, &listener); running.await.map_err(SshError::Io)?; Ok(()) }