增加ssh

增加deploy下细分的tcp、tls、ssh
This commit is contained in:
xiuting.xu
2026-04-22 16:02:42 +08:00
parent 99250f8aa9
commit b60d579a38
37 changed files with 2951 additions and 636 deletions

View File

@@ -48,7 +48,8 @@ impl RtrCache {
project_snapshot_for_version(&source_snapshot, version as u8)
});
let serials = [serial; VERSION_COUNT];
let deltas = std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
let deltas =
std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
tokio::spawn({
let store = store.clone();
@@ -103,7 +104,8 @@ fn try_restore_from_store(
let mut snapshots = std::array::from_fn(|_| Snapshot::empty());
let mut session_ids = [0u16; VERSION_COUNT];
let mut serials = [0u32; VERSION_COUNT];
let mut deltas = std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
let mut deltas =
std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
for version in 0u8..=2 {
let idx = version as usize;
@@ -123,7 +125,8 @@ fn try_restore_from_store(
}
if let Some((min_serial, max_serial)) = store.get_delta_window_for_version(version)? {
let mut loaded = store.load_delta_window_for_version(version, min_serial, max_serial)?;
let mut loaded =
store.load_delta_window_for_version(version, min_serial, max_serial)?;
let max_keep = usize::from(max_delta.max(1));
if loaded.len() > max_keep {
let drop_count = loaded.len() - max_keep;

View File

@@ -1,23 +1,169 @@
use std::collections::HashMap;
use std::future::Future;
use std::net::SocketAddr;
use std::path::Path;
use std::pin::Pin;
use std::sync::{Arc, atomic::AtomicUsize};
use std::time::Duration;
use anyhow::{Context, Result};
use anyhow::{Context, Result, anyhow};
use russh::server::{self, Msg, Session};
use russh::{Channel, ChannelId, Disconnect};
use socket2::{SockRef, TcpKeepalive};
use tokio::net::TcpListener;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{Semaphore, broadcast, watch};
use tracing::{info, warn};
use tokio_rustls::TlsAcceptor;
use tracing::{debug, info, warn};
use rustls::ServerConfig;
use tokio_rustls::TlsAcceptor;
use crate::rtr::cache::SharedRtrCache;
use crate::rtr::server::config::RtrServiceConfig;
use crate::rtr::server::connection::{
ConnectionGuard, handle_tcp_connection, handle_tls_connection, is_expected_disconnect,
};
use crate::rtr::server::ssh::RtrSshRuntimeConfig;
use crate::rtr::server::tls::load_rustls_server_config_with_options;
use crate::rtr::session::RtrSession;
type TransportFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
pub trait TransportAcceptor: Clone + Send + Sync + 'static {
fn name(&self) -> &'static str;
fn handle_connection(
&self,
cache: SharedRtrCache,
stream: TcpStream,
peer_addr: SocketAddr,
notify_tx: broadcast::Sender<()>,
shutdown_tx: watch::Sender<bool>,
) -> TransportFuture;
}
#[derive(Clone)]
struct TcpTransport;
impl TransportAcceptor for TcpTransport {
fn name(&self) -> &'static str {
"TCP"
}
fn handle_connection(
&self,
cache: SharedRtrCache,
stream: TcpStream,
peer_addr: SocketAddr,
notify_tx: broadcast::Sender<()>,
shutdown_tx: watch::Sender<bool>,
) -> TransportFuture {
Box::pin(async move {
handle_tcp_connection(
cache,
stream,
peer_addr,
notify_tx.subscribe(),
shutdown_tx.subscribe(),
)
.await
})
}
}
#[derive(Clone)]
struct TlsTransport {
acceptor: TlsAcceptor,
}
impl TransportAcceptor for TlsTransport {
fn name(&self) -> &'static str {
"TLS"
}
fn handle_connection(
&self,
cache: SharedRtrCache,
stream: TcpStream,
peer_addr: SocketAddr,
notify_tx: broadcast::Sender<()>,
shutdown_tx: watch::Sender<bool>,
) -> TransportFuture {
let acceptor = self.acceptor.clone();
Box::pin(async move {
handle_tls_connection(
cache,
stream,
peer_addr,
acceptor,
notify_tx.subscribe(),
shutdown_tx.subscribe(),
)
.await
})
}
}
#[derive(Clone)]
struct SshTransport {
runtime: Arc<RtrSshRuntimeConfig>,
}
impl TransportAcceptor for SshTransport {
fn name(&self) -> &'static str {
"SSH"
}
fn handle_connection(
&self,
cache: SharedRtrCache,
stream: TcpStream,
peer_addr: SocketAddr,
notify_tx: broadcast::Sender<()>,
shutdown_tx: watch::Sender<bool>,
) -> TransportFuture {
let runtime = self.runtime.clone();
Box::pin(async move {
let handler = RtrSshHandler::new(
cache,
notify_tx.subscribe(),
shutdown_tx.subscribe(),
peer_addr,
runtime.authorized_keys.clone(),
runtime.username.clone(),
runtime.subsystem_name.clone(),
runtime.password.clone(),
);
let running = server::run_stream(runtime.server_config.clone(), stream, handler)
.await
.with_context(|| format!("failed to start SSH session for {}", peer_addr))?;
let handle = running.handle();
let mut connection_shutdown_rx = shutdown_tx.subscribe();
tokio::select! {
session_res = running => {
session_res.map_err(|err| anyhow!(err))
}
changed = connection_shutdown_rx.changed() => {
match changed {
Ok(()) if *connection_shutdown_rx.borrow() => {
let _ = handle
.disconnect(
Disconnect::ByApplication,
"service shutdown".to_string(),
"".to_string(),
)
.await;
}
Ok(()) | Err(_) => {}
}
Ok(())
}
}
})
}
}
pub struct RtrServer {
bind_addr: SocketAddr,
@@ -64,104 +210,7 @@ impl RtrServer {
}
pub async fn run_tcp(self) -> Result<()> {
let listener = TcpListener::bind(self.bind_addr)
.await
.with_context(|| format!("failed to bind TCP RTR server on {}", self.bind_addr))?;
let mut shutdown_rx = self.shutdown_tx.subscribe();
info!("RTR TCP server listening on {}", self.bind_addr);
loop {
tokio::select! {
changed = shutdown_rx.changed() => {
match changed {
Ok(()) => {
if *shutdown_rx.borrow() {
info!("RTR TCP listener {} shutting down", self.bind_addr);
return Ok(());
}
}
Err(_) => {
info!("RTR TCP listener {} shutdown channel closed", self.bind_addr);
return Ok(());
}
}
}
accept_res = listener.accept() => {
let (stream, peer_addr) = match accept_res {
Ok(v) => v,
Err(err) => {
warn!("RTR TCP accept failed: {}", err);
continue;
}
};
if let Err(err) = apply_keepalive(&stream, self.config.tcp_keepalive) {
warn!("failed to configure TCP keepalive for {}: {}", peer_addr, err);
}
let permit = match self.connection_limiter.clone().try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
warn!(
"RTR TCP connection rejected for {}: max connections reached ({})",
peer_addr,
self.config.max_connections
);
drop(stream);
continue;
}
};
let cache = self.cache.clone();
let notify_rx = self.notify_tx.subscribe();
let shutdown_rx = self.shutdown_tx.subscribe();
let active_connections = self.active_connections.clone();
info!(
"RTR TCP client connected: peer_addr={}, active_connections(before_spawn)={}",
peer_addr,
self.active_connections()
);
tokio::spawn(async move {
let guard = ConnectionGuard::new(active_connections, permit);
info!(
"RTR TCP connection established: peer_addr={}, active_connections={}",
peer_addr,
guard.active_count()
);
if let Err(err) =
handle_tcp_connection(cache, stream, peer_addr, notify_rx, shutdown_rx).await
{
if is_expected_disconnect(&err) {
info!(
"RTR TCP session closed by peer: peer_addr={}, active_connections={}, err={}",
peer_addr,
guard.active_count(),
err
);
} else {
warn!(
"RTR TCP session closed with error: peer_addr={}, active_connections={}, err={}",
peer_addr,
guard.active_count(),
err
);
}
} else {
info!(
"RTR TCP session closed cleanly: peer_addr={}, active_connections={}",
peer_addr,
guard.active_count()
);
}
});
}
}
}
self.run_with_transport(TcpTransport).await
}
pub async fn run_tls_from_pem(
@@ -180,14 +229,37 @@ impl RtrServer {
}
pub async fn run_tls(self, tls_config: Arc<ServerConfig>) -> Result<()> {
let listener = TcpListener::bind(self.bind_addr)
.await
.with_context(|| format!("failed to bind TLS RTR server on {}", self.bind_addr))?;
let transport = TlsTransport {
acceptor: TlsAcceptor::from(tls_config),
};
self.run_with_transport(transport).await
}
let acceptor = TlsAcceptor::from(tls_config);
pub async fn run_ssh(self, runtime_config: Arc<RtrSshRuntimeConfig>) -> Result<()> {
let transport = SshTransport {
runtime: runtime_config,
};
self.run_with_transport(transport).await
}
async fn run_with_transport<T>(self, transport: T) -> Result<()>
where
T: TransportAcceptor,
{
let listener = TcpListener::bind(self.bind_addr).await.with_context(|| {
format!(
"failed to bind {} RTR server on {}",
transport.name(),
self.bind_addr
)
})?;
let mut shutdown_rx = self.shutdown_tx.subscribe();
info!("RTR TLS server listening on {}", self.bind_addr);
info!(
"RTR {} server listening on {}",
transport.name(),
self.bind_addr
);
loop {
tokio::select! {
@@ -195,12 +267,20 @@ impl RtrServer {
match changed {
Ok(()) => {
if *shutdown_rx.borrow() {
info!("RTR TLS listener {} shutting down", self.bind_addr);
info!(
"RTR {} listener {} shutting down",
transport.name(),
self.bind_addr
);
return Ok(());
}
}
Err(_) => {
info!("RTR TLS listener {} shutdown channel closed", self.bind_addr);
info!(
"RTR {} listener {} shutdown channel closed",
transport.name(),
self.bind_addr
);
return Ok(());
}
}
@@ -210,20 +290,26 @@ impl RtrServer {
let (stream, peer_addr) = match accept_res {
Ok(v) => v,
Err(err) => {
warn!("RTR TLS accept failed: {}", err);
warn!("RTR {} accept failed: {}", transport.name(), err);
continue;
}
};
if let Err(err) = apply_keepalive(&stream, self.config.tcp_keepalive) {
warn!("failed to configure TCP keepalive for {}: {}", peer_addr, err);
warn!(
"failed to configure TCP keepalive for {} peer {}: {}",
transport.name(),
peer_addr,
err
);
}
let permit = match self.connection_limiter.clone().try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
warn!(
"RTR TLS connection rejected for {}: max connections reached ({})",
"RTR {} connection rejected for {}: max connections reached ({})",
transport.name(),
peer_addr,
self.config.max_connections
);
@@ -233,13 +319,14 @@ impl RtrServer {
};
let cache = self.cache.clone();
let acceptor = acceptor.clone();
let notify_rx = self.notify_tx.subscribe();
let shutdown_rx = self.shutdown_tx.subscribe();
let notify_tx = self.notify_tx.clone();
let shutdown_tx = self.shutdown_tx.clone();
let active_connections = self.active_connections.clone();
let transport_instance = transport.clone();
info!(
"RTR TLS client connected: peer_addr={}, active_connections(before_spawn)={}",
debug!(
"RTR {} client connected: peer_addr={}, active_connections(before_spawn)={}",
transport_instance.name(),
peer_addr,
self.active_connections()
);
@@ -247,38 +334,41 @@ impl RtrServer {
tokio::spawn(async move {
let guard = ConnectionGuard::new(active_connections, permit);
info!(
"RTR TLS connection established: peer_addr={}, active_connections={}",
"RTR {} connection established: peer_addr={}, active_connections={}",
transport_instance.name(),
peer_addr,
guard.active_count()
);
if let Err(err) = handle_tls_connection(
cache,
stream,
peer_addr,
acceptor,
notify_rx,
shutdown_rx,
).await {
if let Err(err) = transport_instance
.handle_connection(cache, stream, peer_addr, notify_tx, shutdown_tx)
.await
{
let active_after_close = guard.active_count().saturating_sub(1);
if is_expected_disconnect(&err) {
info!(
"RTR TLS session closed by peer: peer_addr={}, active_connections={}, err={}",
"RTR {} session closed by peer: peer_addr={}, active_connections={}, err={}",
transport_instance.name(),
peer_addr,
guard.active_count(),
active_after_close,
err
);
} else {
warn!(
"RTR TLS session closed with error: peer_addr={}, active_connections={}, err={}",
"RTR {} session closed with error: peer_addr={}, active_connections={}, err={}",
transport_instance.name(),
peer_addr,
guard.active_count(),
active_after_close,
err
);
}
} else {
let active_after_close = guard.active_count().saturating_sub(1);
info!(
"RTR TLS session closed cleanly: peer_addr={}, active_connections={}",
"RTR {} session closed cleanly: peer_addr={}, active_connections={}",
transport_instance.name(),
peer_addr,
guard.active_count()
active_after_close
);
}
});
@@ -288,6 +378,215 @@ impl RtrServer {
}
}
struct RtrSshHandler {
cache: SharedRtrCache,
notify_rx: broadcast::Receiver<()>,
shutdown_rx: watch::Receiver<bool>,
peer_addr: SocketAddr,
authorized_keys: Arc<Vec<russh::keys::ssh_key::PublicKey>>,
username: Arc<str>,
subsystem_name: Arc<str>,
password: Option<Arc<str>>,
channels: HashMap<ChannelId, Channel<Msg>>,
subsystem_started: bool,
}
impl RtrSshHandler {
fn new(
cache: SharedRtrCache,
notify_rx: broadcast::Receiver<()>,
shutdown_rx: watch::Receiver<bool>,
peer_addr: SocketAddr,
authorized_keys: Arc<Vec<russh::keys::ssh_key::PublicKey>>,
username: Arc<str>,
subsystem_name: Arc<str>,
password: Option<Arc<str>>,
) -> Self {
Self {
cache,
notify_rx,
shutdown_rx,
peer_addr,
authorized_keys,
username,
subsystem_name,
password,
channels: HashMap::new(),
subsystem_started: false,
}
}
fn is_authorized_key(&self, key: &russh::keys::ssh_key::PublicKey) -> bool {
self.authorized_keys
.iter()
.any(|allowed| allowed.key_data() == key.key_data())
}
fn is_expected_user(&self, user: &str) -> bool {
user == self.username.as_ref()
}
}
impl server::Handler for RtrSshHandler {
type Error = anyhow::Error;
async fn auth_none(&mut self, _user: &str) -> Result<server::Auth, Self::Error> {
Ok(server::Auth::reject())
}
async fn auth_password(
&mut self,
user: &str,
password: &str,
) -> Result<server::Auth, Self::Error> {
let accepted = self.is_expected_user(user)
&& self
.password
.as_deref()
.map(|expected| expected == password)
.unwrap_or(false);
if accepted {
info!(
"RTR SSH password auth accepted: peer_addr={}, user={}",
self.peer_addr, user
);
Ok(server::Auth::Accept)
} else {
warn!(
"RTR SSH password auth rejected: peer_addr={}, user={}",
self.peer_addr, user
);
Ok(server::Auth::reject())
}
}
async fn auth_publickey_offered(
&mut self,
user: &str,
public_key: &russh::keys::ssh_key::PublicKey,
) -> Result<server::Auth, Self::Error> {
if self.is_expected_user(user) && self.is_authorized_key(public_key) {
Ok(server::Auth::Accept)
} else {
Ok(server::Auth::reject())
}
}
async fn auth_publickey(
&mut self,
user: &str,
public_key: &russh::keys::ssh_key::PublicKey,
) -> Result<server::Auth, Self::Error> {
if self.is_expected_user(user) && self.is_authorized_key(public_key) {
info!(
"RTR SSH publickey auth accepted: peer_addr={}, user={}",
self.peer_addr, user
);
Ok(server::Auth::Accept)
} else {
warn!(
"RTR SSH publickey auth rejected: peer_addr={}, user={}",
self.peer_addr, user
);
Ok(server::Auth::reject())
}
}
async fn channel_open_session(
&mut self,
channel: Channel<Msg>,
_session: &mut Session,
) -> Result<bool, Self::Error> {
if self.subsystem_started {
return Ok(false);
}
self.channels.insert(channel.id(), channel);
Ok(true)
}
async fn subsystem_request(
&mut self,
channel: ChannelId,
name: &str,
session: &mut Session,
) -> Result<(), Self::Error> {
if name != self.subsystem_name.as_ref() {
let _ = session.channel_failure(channel);
warn!(
"RTR SSH subsystem rejected: peer_addr={}, requested={}, expected={}",
self.peer_addr, name, self.subsystem_name
);
return Ok(());
}
let Some(channel) = self.channels.remove(&channel) else {
let _ = session.channel_failure(channel);
return Ok(());
};
session.channel_success(channel.id())?;
self.subsystem_started = true;
let cache = self.cache.clone();
let notify_rx = self.notify_rx.resubscribe();
let shutdown_rx = self.shutdown_rx.clone();
let peer_addr = self.peer_addr;
let subsystem = self.subsystem_name.clone();
tokio::spawn(async move {
let stream = channel.into_stream();
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx);
if let Err(err) = session.run().await {
warn!(
"RTR SSH subsystem session closed with error: peer_addr={}, subsystem={}, err={}",
peer_addr, subsystem, err
);
} else {
info!(
"RTR SSH subsystem session completed: peer_addr={}, subsystem={}",
peer_addr, subsystem
);
}
});
Ok(())
}
async fn shell_request(
&mut self,
channel: ChannelId,
session: &mut Session,
) -> Result<(), Self::Error> {
let _ = session.channel_failure(channel);
Ok(())
}
async fn exec_request(
&mut self,
channel: ChannelId,
_data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
let _ = session.channel_failure(channel);
Ok(())
}
async fn pty_request(
&mut self,
channel: ChannelId,
_term: &str,
_col_width: u32,
_row_height: u32,
_pix_width: u32,
_pix_height: u32,
_modes: &[(russh::Pty, u32)],
session: &mut Session,
) -> Result<(), Self::Error> {
let _ = session.channel_failure(channel);
Ok(())
}
}
fn apply_keepalive(stream: &tokio::net::TcpStream, keepalive: Option<Duration>) -> Result<()> {
let Some(keepalive) = keepalive else {
return Ok(());

View File

@@ -3,6 +3,7 @@ pub mod connection;
pub mod listener;
pub mod notifier;
pub mod service;
pub mod ssh;
pub mod tls;
pub use config::RtrServiceConfig;

View File

@@ -13,6 +13,7 @@ use crate::rtr::cache::SharedRtrCache;
use crate::rtr::server::config::RtrServiceConfig;
use crate::rtr::server::listener::RtrServer;
use crate::rtr::server::notifier::RtrNotifier;
use crate::rtr::server::ssh::load_rtr_ssh_runtime_config;
pub struct RtrService {
cache: SharedRtrCache,
@@ -86,6 +87,18 @@ impl RtrService {
)
}
pub fn ssh_server(&self, bind_addr: SocketAddr) -> RtrServer {
RtrServer::new(
bind_addr,
self.cache.clone(),
self.notify_tx.clone(),
self.shutdown_tx.clone(),
self.connection_limiter.clone(),
self.active_connections.clone(),
self.config.clone(),
)
}
pub fn spawn_tcp(&self, bind_addr: SocketAddr) -> JoinHandle<()> {
if self.config.warn_insecure_tcp {
warn!(
@@ -141,6 +154,111 @@ impl RtrService {
}
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_ssh_from_openssh(
&self,
bind_addr: SocketAddr,
host_key_path: impl AsRef<Path>,
authorized_keys_path: impl AsRef<Path>,
username: &str,
subsystem_name: &str,
password: Option<&str>,
) -> JoinHandle<()> {
let host_key_path = host_key_path.as_ref().to_path_buf();
let authorized_keys_path = authorized_keys_path.as_ref().to_path_buf();
let username = username.to_string();
let subsystem_name = subsystem_name.to_string();
let password = password.map(ToString::to_string);
let inactivity_timeout = Some(std::time::Duration::from_secs(3600));
let keepalive_interval = self.config.tcp_keepalive;
let server = self.ssh_server(bind_addr);
tokio::spawn(async move {
let runtime_config = match load_rtr_ssh_runtime_config(
&host_key_path,
&authorized_keys_path,
&username,
&subsystem_name,
password.as_deref(),
inactivity_timeout,
keepalive_interval,
) {
Ok(cfg) => Arc::new(cfg),
Err(err) => {
error!(
"RTR SSH server {} failed to load configuration: {:?}",
bind_addr, err
);
return;
}
};
if let Err(err) = server.run_ssh(runtime_config).await {
error!("RTR SSH server {} exited with error: {:?}", bind_addr, err);
}
})
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_tcp_and_ssh_from_openssh(
&self,
tcp_bind_addr: SocketAddr,
ssh_bind_addr: SocketAddr,
host_key_path: impl AsRef<Path>,
authorized_keys_path: impl AsRef<Path>,
username: &str,
subsystem_name: &str,
password: Option<&str>,
) -> RunningRtrService {
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
let ssh_handle = self.spawn_ssh_from_openssh(
ssh_bind_addr,
host_key_path,
authorized_keys_path,
username,
subsystem_name,
password,
);
RunningRtrService {
shutdown_tx: self.shutdown_tx.clone(),
handles: vec![tcp_handle, ssh_handle],
}
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_tcp_tls_and_ssh_from_pem_and_openssh(
&self,
tcp_bind_addr: SocketAddr,
tls_bind_addr: SocketAddr,
ssh_bind_addr: SocketAddr,
cert_path: impl AsRef<Path>,
key_path: impl AsRef<Path>,
client_ca_path: impl AsRef<Path>,
host_key_path: impl AsRef<Path>,
authorized_keys_path: impl AsRef<Path>,
username: &str,
subsystem_name: &str,
password: Option<&str>,
) -> RunningRtrService {
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
let tls_handle =
self.spawn_tls_from_pem(tls_bind_addr, cert_path, key_path, client_ca_path);
let ssh_handle = self.spawn_ssh_from_openssh(
ssh_bind_addr,
host_key_path,
authorized_keys_path,
username,
subsystem_name,
password,
);
RunningRtrService {
shutdown_tx: self.shutdown_tx.clone(),
handles: vec![tcp_handle, tls_handle, ssh_handle],
}
}
pub fn spawn_tcp_only(&self, tcp_bind_addr: SocketAddr) -> RunningRtrService {
let tcp_handle = self.spawn_tcp(tcp_bind_addr);

97
src/rtr/server/ssh.rs Normal file
View File

@@ -0,0 +1,97 @@
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)]
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,
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 = load_authorized_keys(authorized_keys_path.as_ref())?;
let password = password.map(str::trim).filter(|value| !value.is_empty());
let mut methods = MethodSet::empty();
methods.push(MethodKind::PublicKey);
if password.is_some() {
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)
}

View File

@@ -102,19 +102,43 @@ where
}
}
header_res = timeout(transport_timeout, Header::read_raw(&mut self.stream)) => {
header_res = async {
// draft-ietf-sidrops-8210bis-25 Section 6 allows routers to wait up to
// Refresh Interval before polling again (recommended default: 3600s).
// In an established session, a long quiet period is therefore expected and
// must not be treated as a transport stall.
//
// We only enforce transport timeout before session establishment. Once the
// session is established, we wait indefinitely for the next query header.
if self.state == SessionState::Established {
Header::read_raw(&mut self.stream).await
} else {
timeout(transport_timeout, Header::read_raw(&mut self.stream))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "transport read timed out"))?
}
} => {
let raw_header = match header_res {
Ok(Ok(raw)) => raw,
Ok(Err(_)) => {
Ok(raw) => raw,
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
info!("RTR session closed by peer before header read completed: {}", self.session_summary());
self.state = SessionState::Closed;
return Ok(());
}
Err(_) => {
Err(err) if err.kind() == io::ErrorKind::TimedOut => {
warn!("RTR session transport timeout while waiting for header: {}", self.session_summary());
self.handle_transport_timeout(&[]).await?;
return Ok(());
}
Err(err) => {
warn!(
"RTR session failed to read header: err={}, {}",
err,
self.session_summary()
);
self.state = SessionState::Closed;
return Ok(());
}
};
let header = match Header::from_raw(raw_header) {
Ok(h) => h,
@@ -546,7 +570,7 @@ where
}
async fn handle_reset_query(&mut self, offending_pdu: &[u8]) -> Result<()> {
info!(
debug!(
"RTR session received Reset Query: negotiated_version={:?}, offending_pdu_len={}",
self.version,
offending_pdu.len()
@@ -568,7 +592,7 @@ where
if !data_available {
self.send_no_data_available(offending_pdu, "cache data is not currently available")
.await?;
info!(
debug!(
"RTR session replied No Data Available to Reset Query: {}",
self.session_summary()
);
@@ -578,7 +602,7 @@ where
self.write_cache_response(session_id).await?;
self.send_payloads(&payloads, true).await?;
self.write_end_of_data(session_id, serial).await?;
info!(
debug!(
"RTR session completed Reset Query: response_session_id={}, response_serial={}, payload_count={}, {}",
session_id,
serial,
@@ -596,7 +620,7 @@ where
client_serial: u32,
offending_pdu: &[u8],
) -> Result<()> {
info!(
debug!(
"RTR session received Serial Query: negotiated_version={}, client_session_id={}, client_serial={}, offending_pdu_len={}",
version,
client_session,
@@ -617,7 +641,7 @@ where
if !data_available {
self.send_no_data_available(offending_pdu, "cache data is not currently available")
.await?;
info!(
debug!(
"RTR session replied No Data Available to Serial Query: client_session_id={}, client_serial={}, {}",
client_session,
client_serial,
@@ -650,7 +674,7 @@ where
match serial_result {
SerialResult::ResetRequired => {
self.write_cache_reset().await?;
info!(
debug!(
"RTR session replied Cache Reset to Serial Query: client_session_id={}, client_serial={}, {}",
client_session,
client_serial,
@@ -668,13 +692,13 @@ where
(
cache.session_id_for_version(version),
cache.serial_for_version(version),
)
)
};
self.write_cache_response(current_session).await?;
self.write_end_of_data(current_session, current_serial)
.await?;
info!(
debug!(
"RTR session replied CacheResponse+EndOfData (up-to-date) to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
client_session,
client_serial,
@@ -701,7 +725,7 @@ where
self.send_delta(&delta).await?;
self.write_end_of_data(current_session, current_serial)
.await?;
info!(
debug!(
"RTR session replied delta to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
client_session,
client_serial,
@@ -782,7 +806,10 @@ where
.cache
.read()
.ok()
.and_then(|cache| self.version.map(|version| cache.serial_for_version(version)))
.and_then(|cache| {
self.version
.map(|version| cache.serial_for_version(version))
})
.map(|serial| serial.to_string())
.unwrap_or_else(|| "<unavailable>".to_string());
let session_id = self