增加ssh
增加deploy下细分的tcp、tls、ssh
This commit is contained in:
9
src/rtr/cache/store.rs
vendored
9
src/rtr/cache/store.rs
vendored
@@ -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;
|
||||
|
||||
@@ -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(());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
97
src/rtr/server/ssh.rs
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user