pub extern crate russh;
use std::{fmt::Debug, net::SocketAddr, sync::Arc};
const MAX_SSH_CONNECTIONS: usize = 64;
use russh::server::Handler;
use ts_control::SshConnIdentity;
pub use ts_control::{SshAccept, SshDecision, SshDenyReason, SshPolicy};
mod channel_server;
mod channel_write;
mod ratatui;
mod shell;
pub use channel_server::{ChannelEvent, ChannelHandler, ChannelServer};
pub use ratatui::{RatatuiApp, RatatuiEnv, RatatuiTerm};
pub use shell::ShellHandler;
impl crate::Device {
pub async fn authorize_ssh(
&self,
remote: SocketAddr,
requested_user: &str,
) -> Result<SshDecision, crate::Error> {
use ts_control::SshDenyReason;
let Some(peer) = self.peer_by_tailnet_ip(remote.ip()).await? else {
tracing::warn!(remote = %remote, "ssh: source IP does not match a known tailnet peer");
return Ok(SshDecision::Deny(SshDenyReason::NoRuleMatched));
};
let Some(policy) = self.ssh_policy().await? else {
tracing::warn!(remote = %remote, "ssh: no SSH policy pushed by control; deny-all");
return Ok(SshDecision::Deny(SshDenyReason::NoRuleMatched));
};
let id = SshConnIdentity {
stable_id: peer.stable_id.0.clone(),
src_ip: remote.ip(),
user_login: None,
};
Ok(policy.evaluate_at_unix(&id, requested_user, now_unix_secs()))
}
}
fn now_unix_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(i64::MAX)
}
pub trait TailnetServer {
fn new_client(dev: Arc<crate::Device>, addr: SocketAddr) -> Self;
}
impl crate::Device {
pub async fn serve_ssh<H>(
self: Arc<Self>,
config: russh::server::Config,
listen_addr: SocketAddr,
) -> Result<(), crate::Error>
where
H: TailnetServer + Handler + Send + 'static,
H::Error: Debug,
{
let config = Arc::new(config);
let listener = self.tcp_listen(listen_addr).await?;
tracing::info!(%listen_addr, "ssh server listening");
let sem = Arc::new(tokio::sync::Semaphore::new(MAX_SSH_CONNECTIONS));
let mut sessions = tokio::task::JoinSet::new();
loop {
while sessions.try_join_next().is_some() {}
let Ok(permit) = sem.clone().acquire_owned().await else {
return Ok(());
};
let conn = listener.accept().await?;
let handler = H::new_client(self.clone(), conn.remote_addr());
let config = config.clone();
sessions.spawn(async move {
let _permit = permit;
let sess = match russh::server::run_stream(config, conn, handler).await {
Ok(sess) => sess,
Err(e) => {
tracing::error!(error = ?e, "establishing session");
return;
}
};
match sess.await {
Ok(()) => {}
Err(e) => {
tracing::error!(error = ?e, "running ssh session");
}
}
});
}
}
pub async fn listen_ssh(
self: Arc<Self>,
config: russh::server::Config,
listen_addr: SocketAddr,
) -> Result<(), crate::Error> {
self.serve_ssh::<ChannelServer<ShellHandler>>(config, listen_addr)
.await
}
pub async fn serve_ssh_tui<App>(
self: Arc<Self>,
config: russh::server::Config,
listen_addr: SocketAddr,
) -> Result<(), crate::Error>
where
App: RatatuiApp + Default + Send + 'static,
{
self.serve_ssh::<ChannelServer<RatatuiTerm<App>>>(config, listen_addr)
.await
}
}