pub mod handler;
pub(crate) mod shell_access;
pub(crate) mod side_streams;
pub(crate) mod startup;
pub(crate) mod transfer;
use russh::keys::ssh_key::PublicKey;
use russh::server;
use std::fmt;
use std::sync::Arc;
use tracing::{info, warn};
use crate::config::{SecurityConfig, StateConfig};
use crate::error::Result;
use crate::server::handler::ServerHandler;
use crate::server::side_streams::spawn_metadata_and_transfer_acceptor;
use crate::server::startup::{bind_server, inspect_server};
pub(crate) use crate::server::transfer::ConnectionShellState;
use crate::transport::stream::IrohDuplex;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerOptions {
state: StateConfig,
security: SecurityConfig,
secret: Option<String>,
authorized_keys: Vec<russh::keys::ssh_key::PublicKey>,
}
impl ServerOptions {
pub fn new(state: StateConfig) -> Self {
Self {
state,
security: SecurityConfig::default(),
secret: None,
authorized_keys: Vec::new(),
}
}
pub fn security(mut self, security: SecurityConfig) -> Self {
self.security = security;
self
}
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret = Some(secret.into());
self
}
pub fn authorized_key(mut self, key: russh::keys::ssh_key::PublicKey) -> Self {
self.authorized_keys.push(key);
self
}
pub fn authorized_keys(
mut self,
keys: impl IntoIterator<Item = russh::keys::ssh_key::PublicKey>,
) -> Self {
self.authorized_keys = keys.into_iter().collect();
self
}
pub(crate) fn state(&self) -> &StateConfig {
&self.state
}
pub(crate) fn security_config(&self) -> SecurityConfig {
self.security
}
pub(crate) fn secret_value(&self) -> Option<&str> {
self.secret.as_deref()
}
pub(crate) fn authorized_key_list(&self) -> &[russh::keys::ssh_key::PublicKey] {
&self.authorized_keys
}
}
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerReady {
endpoint_id: String,
ticket: crate::transport::ticket::Ticket,
relay_urls: Vec<String>,
direct_addresses: Vec<String>,
host_key_openssh: String,
}
impl ServerReady {
pub fn endpoint_id(&self) -> &str {
&self.endpoint_id
}
pub fn ticket(&self) -> &crate::transport::ticket::Ticket {
&self.ticket
}
pub fn relay_urls(&self) -> &[String] {
&self.relay_urls
}
pub fn direct_addresses(&self) -> &[String] {
&self.direct_addresses
}
pub fn host_key_openssh(&self) -> &str {
&self.host_key_openssh
}
}
pub struct Server {
endpoint: iroh::Endpoint,
config: Arc<server::Config>,
authorized_clients: Vec<PublicKey>,
security: SecurityConfig,
state: StateConfig,
secret: Option<String>,
}
impl fmt::Debug for Server {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Server")
.field("authorized_clients", &self.authorized_clients.len())
.field("security", &self.security)
.field("state", &self.state)
.field("has_secret", &self.secret.is_some())
.finish()
}
}
#[derive(Clone, Debug)]
pub struct ServerShutdown {
endpoint: iroh::Endpoint,
}
impl ServerShutdown {
pub async fn close(&self) {
self.endpoint.close().await;
}
}
impl Server {
pub async fn inspect(options: ServerOptions) -> Result<ServerReady> {
inspect_server(&options).await
}
pub async fn bind(options: ServerOptions) -> Result<(ServerReady, Self)> {
bind_server(options).await
}
pub fn shutdown_handle(&self) -> ServerShutdown {
ServerShutdown {
endpoint: self.endpoint.clone(),
}
}
pub async fn run(self) -> Result<()> {
info!("Server actively listening for connections.");
while let Some(incoming) = self.endpoint.accept().await {
let mut accepting = match incoming.accept() {
Ok(accepting) => accepting,
Err(err) => {
warn!("Incoming connection rejected before ALPN exchange: {err}");
continue;
}
};
let alpn = match accepting.alpn().await {
Ok(alpn) => alpn,
Err(err) => {
warn!("Failed ALPN read: {}", err);
continue;
}
};
if alpn != crate::transport::iroh::derive_alpn(self.secret.as_deref()) {
warn!(
"Ignoring unexpected ALPN: {}",
String::from_utf8_lossy(&alpn)
);
continue;
}
let conn = match accepting.await {
Ok(conn) => conn,
Err(err) => {
warn!("P2P connection handshake failed: {}", err);
continue;
}
};
let (send, recv) = match conn.accept_bi().await {
Ok(pair) => pair,
Err(err) => {
warn!("Failed to establish bi-directional stream: {}", err);
continue;
}
};
info!("Established bi-directional SSH stream over Irosh");
let shell_state = ConnectionShellState::new();
spawn_metadata_and_transfer_acceptor(conn, shell_state.clone());
let stream = IrohDuplex::new(send, recv);
let handler = ServerHandler::new(
self.authorized_clients.clone(),
self.security,
self.state.clone(),
shell_state,
);
let config = self.config.clone();
tokio::spawn(async move {
if let Err(err) = server::run_stream(config, stream, handler).await {
warn!("Server session error: {:?}", err);
}
});
}
self.endpoint.close().await;
info!("Server shut down gracefully.");
Ok(())
}
}
#[cfg(test)]
mod tests;