feat: remote SSH proxy mode
Bascule now supports two session modes: Local — spawns a PTY on this machine (default, existing) Proxy — forwards the session to a target SSH host (NEW) Proxy mode: SSH client ←→ bascule (auth + hooks) ←→ target SSH host Authenticates client via configured auth provider Connects to upstream SSH host via russh client Bridges I/O between client and upstream channels PTY, shell, and exec requests forwarded to target Exit status propagated back to client Config: [proxy] target_host = "192.168.1.100" target_port = 22 target_user = "deploy" # optional, defaults to principal target_key_path = "/etc/bascule/target_key" accept_target_host_key = false # dev only SessionHandler hooks fire in both modes: on_session_start, on_exec, on_session_end Custom handlers can enforce policy regardless of mode New file: proxy.rs (152 lines) UpstreamHandler — minimal russh client handler connect_upstream — connects + authenticates to target bridge_upstream_to_client — bidirectional I/O bridge Binary: 6.3MB, zero substrate deps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02142f7be4
commit
2212f7f870
4 changed files with 294 additions and 21 deletions
|
|
@ -29,6 +29,29 @@ pub struct BasculeConfig {
|
||||||
/// Maximum concurrent sessions (0 = unlimited).
|
/// Maximum concurrent sessions (0 = unlimited).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub max_sessions: usize,
|
pub max_sessions: usize,
|
||||||
|
|
||||||
|
/// Remote proxy configuration.
|
||||||
|
/// When set, sessions are forwarded to a target SSH host
|
||||||
|
/// instead of spawning a local shell.
|
||||||
|
pub proxy: Option<ProxyConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ProxyConfig {
|
||||||
|
/// Target SSH host to forward sessions to.
|
||||||
|
pub target_host: String,
|
||||||
|
/// Target SSH port (default: 22).
|
||||||
|
#[serde(default = "default_ssh_port")]
|
||||||
|
pub target_port: u16,
|
||||||
|
/// Username on the target host.
|
||||||
|
/// If not set, uses the authenticated principal.
|
||||||
|
pub target_user: Option<String>,
|
||||||
|
/// Path to private key for target host authentication.
|
||||||
|
/// If not set, uses agent forwarding or password from the client.
|
||||||
|
pub target_key_path: Option<String>,
|
||||||
|
/// Accept any host key from target (dev only — disable in production).
|
||||||
|
#[serde(default)]
|
||||||
|
pub accept_target_host_key: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Default)]
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
|
@ -66,6 +89,7 @@ impl Default for BasculeConfig {
|
||||||
auth: AuthConfig::default(),
|
auth: AuthConfig::default(),
|
||||||
banner: None,
|
banner: None,
|
||||||
max_sessions: 0,
|
max_sessions: 0,
|
||||||
|
proxy: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,3 +112,7 @@ fn default_listen() -> String {
|
||||||
fn default_auth_mode() -> String {
|
fn default_auth_mode() -> String {
|
||||||
"accept-all".to_string()
|
"accept-all".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_ssh_port() -> u16 {
|
||||||
|
22
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
//! SSH handler — implements russh's Handler trait.
|
//! SSH handler — implements russh's Handler trait.
|
||||||
//!
|
//!
|
||||||
//! Bridges SSH protocol events to PTY sessions with SessionHandler hooks.
|
//! Supports two modes:
|
||||||
|
//! - **Local mode** — spawns a PTY on this machine (default)
|
||||||
|
//! - **Proxy mode** — forwards the session to a target SSH host
|
||||||
|
//!
|
||||||
|
//! SessionHandler hooks fire in both modes at the same lifecycle points.
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -14,16 +18,23 @@ use tokio::sync::Mutex;
|
||||||
use crate::auth::AuthProvider;
|
use crate::auth::AuthProvider;
|
||||||
use crate::config::BasculeConfig;
|
use crate::config::BasculeConfig;
|
||||||
use crate::hooks::SessionHandler;
|
use crate::hooks::SessionHandler;
|
||||||
|
use crate::proxy::{self, UpstreamSession};
|
||||||
use crate::pty::{self, PtyBridge};
|
use crate::pty::{self, PtyBridge};
|
||||||
use crate::session::SessionInfo;
|
use crate::session::SessionInfo;
|
||||||
|
|
||||||
|
/// Backend for an active session — either a local PTY or a remote proxy.
|
||||||
|
enum SessionBackend {
|
||||||
|
Local(Arc<Mutex<PtyBridge>>),
|
||||||
|
Proxy(Arc<Mutex<UpstreamSession>>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-connection SSH handler.
|
/// Per-connection SSH handler.
|
||||||
pub struct BasculeHandler {
|
pub struct BasculeHandler {
|
||||||
auth: Arc<dyn AuthProvider>,
|
auth: Arc<dyn AuthProvider>,
|
||||||
session_handler: Arc<dyn SessionHandler>,
|
session_handler: Arc<dyn SessionHandler>,
|
||||||
config: Arc<BasculeConfig>,
|
config: Arc<BasculeConfig>,
|
||||||
session_info: Option<SessionInfo>,
|
session_info: Option<SessionInfo>,
|
||||||
pty_bridge: Option<Arc<Mutex<PtyBridge>>>,
|
backend: Option<SessionBackend>,
|
||||||
pty_cols: u16,
|
pty_cols: u16,
|
||||||
pty_rows: u16,
|
pty_rows: u16,
|
||||||
peer_addr: String,
|
peer_addr: String,
|
||||||
|
|
@ -41,7 +52,7 @@ impl BasculeHandler {
|
||||||
session_handler,
|
session_handler,
|
||||||
config,
|
config,
|
||||||
session_info: None,
|
session_info: None,
|
||||||
pty_bridge: None,
|
backend: None,
|
||||||
pty_cols: 80,
|
pty_cols: 80,
|
||||||
pty_rows: 24,
|
pty_rows: 24,
|
||||||
peer_addr,
|
peer_addr,
|
||||||
|
|
@ -55,8 +66,12 @@ impl BasculeHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn PTY, start read loop, return Ok.
|
fn is_proxy_mode(&self) -> bool {
|
||||||
async fn spawn_shell(
|
self.config.proxy.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start local PTY mode — spawn a shell on this machine.
|
||||||
|
async fn start_local(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
session: &mut Session,
|
session: &mut Session,
|
||||||
|
|
@ -66,16 +81,13 @@ impl BasculeHandler {
|
||||||
.ok_or_else(|| anyhow::anyhow!("No session info"))?
|
.ok_or_else(|| anyhow::anyhow!("No session info"))?
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
// SessionHandler hook
|
|
||||||
self.session_handler.on_session_start(&info).await?;
|
self.session_handler.on_session_start(&info).await?;
|
||||||
|
|
||||||
// Build env
|
|
||||||
let mut env = self.session_handler.build_session_env(&info).await;
|
let mut env = self.session_handler.build_session_env(&info).await;
|
||||||
env.insert("BASCULE_SESSION_ID".into(), info.session_id.clone());
|
env.insert("BASCULE_SESSION_ID".into(), info.session_id.clone());
|
||||||
env.insert("BASCULE_PRINCIPAL".into(), info.principal.clone());
|
env.insert("BASCULE_PRINCIPAL".into(), info.principal.clone());
|
||||||
let env_pairs: Vec<(String, String)> = env.into_iter().collect();
|
let env_pairs: Vec<(String, String)> = env.into_iter().collect();
|
||||||
|
|
||||||
// Determine command
|
|
||||||
let (cmd, args) = match command {
|
let (cmd, args) = match command {
|
||||||
Some(c) => ("/bin/sh", vec!["-c".to_string(), c.to_string()]),
|
Some(c) => ("/bin/sh", vec!["-c".to_string(), c.to_string()]),
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -86,9 +98,8 @@ impl BasculeHandler {
|
||||||
|
|
||||||
let bridge = pty::spawn_pty(cmd, &args, env_pairs, self.pty_cols, self.pty_rows)?;
|
let bridge = pty::spawn_pty(cmd, &args, env_pairs, self.pty_cols, self.pty_rows)?;
|
||||||
let bridge = Arc::new(Mutex::new(bridge));
|
let bridge = Arc::new(Mutex::new(bridge));
|
||||||
self.pty_bridge = Some(bridge.clone());
|
self.backend = Some(SessionBackend::Local(bridge.clone()));
|
||||||
|
|
||||||
// Start read loop
|
|
||||||
let handle = session.handle();
|
let handle = session.handle();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut buf = [0u8; 4096];
|
let mut buf = [0u8; 4096];
|
||||||
|
|
@ -109,6 +120,72 @@ impl BasculeHandler {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start proxy mode — connect to upstream SSH host and bridge I/O.
|
||||||
|
async fn start_proxy(
|
||||||
|
&mut self,
|
||||||
|
channel: ChannelId,
|
||||||
|
session: &mut Session,
|
||||||
|
command: Option<&str>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let info = self.session_info.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No session info"))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
self.session_handler.on_session_start(&info).await?;
|
||||||
|
|
||||||
|
let proxy_config = self.config.proxy.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Proxy config missing"))?;
|
||||||
|
|
||||||
|
let mut upstream = proxy::connect_upstream(proxy_config, &info.principal).await?;
|
||||||
|
|
||||||
|
// Request PTY on the upstream channel
|
||||||
|
let upstream_ch = upstream.channel.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No upstream channel"))?;
|
||||||
|
upstream_ch.request_pty(
|
||||||
|
true,
|
||||||
|
"xterm-256color",
|
||||||
|
self.pty_cols as u32,
|
||||||
|
self.pty_rows as u32,
|
||||||
|
0, 0,
|
||||||
|
&[],
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Request shell or exec on upstream
|
||||||
|
match command {
|
||||||
|
Some(cmd) => upstream_ch.exec(true, cmd).await?,
|
||||||
|
None => upstream_ch.request_shell(true).await?,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the channel out for the bridge loop (bridge owns it)
|
||||||
|
let upstream_channel = upstream.channel.take()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Channel already taken"))?;
|
||||||
|
|
||||||
|
let upstream = Arc::new(Mutex::new(upstream));
|
||||||
|
self.backend = Some(SessionBackend::Proxy(upstream.clone()));
|
||||||
|
|
||||||
|
// Bridge upstream → client
|
||||||
|
let server_handle = session.handle();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
proxy::bridge_upstream_to_client(upstream_channel, server_handle, channel).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start session in the appropriate mode.
|
||||||
|
async fn start_session(
|
||||||
|
&mut self,
|
||||||
|
channel: ChannelId,
|
||||||
|
session: &mut Session,
|
||||||
|
command: Option<&str>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if self.is_proxy_mode() {
|
||||||
|
self.start_proxy(channel, session, command).await
|
||||||
|
} else {
|
||||||
|
self.start_local(channel, session, command).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -183,10 +260,19 @@ impl Handler for BasculeHandler {
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
self.pty_cols = col_width.min(500) as u16;
|
self.pty_cols = col_width.min(500) as u16;
|
||||||
self.pty_rows = row_height.min(200) as u16;
|
self.pty_rows = row_height.min(200) as u16;
|
||||||
if let Some(bridge) = &self.pty_bridge {
|
match &self.backend {
|
||||||
|
Some(SessionBackend::Local(bridge)) => {
|
||||||
let b = bridge.lock().await;
|
let b = bridge.lock().await;
|
||||||
let _ = b.resize(self.pty_cols, self.pty_rows);
|
let _ = b.resize(self.pty_cols, self.pty_rows);
|
||||||
}
|
}
|
||||||
|
Some(SessionBackend::Proxy(_)) => {
|
||||||
|
// Window change in proxy mode requires the channel reference
|
||||||
|
// which is owned by the bridge loop. The terminal may not
|
||||||
|
// resize perfectly but the session continues.
|
||||||
|
tracing::debug!("Window change in proxy mode (not forwarded)");
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,7 +281,6 @@ impl Handler for BasculeHandler {
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
session: &mut Session,
|
session: &mut Session,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
// Send banner
|
|
||||||
if let Some(info) = &self.session_info {
|
if let Some(info) = &self.session_info {
|
||||||
let display = self.session_handler.display_name(info);
|
let display = self.session_handler.display_name(info);
|
||||||
let banner = self.config.banner.as_deref()
|
let banner = self.config.banner.as_deref()
|
||||||
|
|
@ -205,7 +290,7 @@ impl Handler for BasculeHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
session.request_success();
|
session.request_success();
|
||||||
self.spawn_shell(channel, session, None).await?;
|
self.start_session(channel, session, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,12 +307,12 @@ impl Handler for BasculeHandler {
|
||||||
let msg = format!("Denied: {}\r\n", e);
|
let msg = format!("Denied: {}\r\n", e);
|
||||||
session.data(channel, CryptoVec::from_slice(msg.as_bytes()));
|
session.data(channel, CryptoVec::from_slice(msg.as_bytes()));
|
||||||
session.close(channel);
|
session.close(channel);
|
||||||
return Ok(()); // close returns ()
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.request_success();
|
session.request_success();
|
||||||
self.spawn_shell(channel, session, Some(&command)).await?;
|
self.start_session(channel, session, Some(&command)).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,11 +322,18 @@ impl Handler for BasculeHandler {
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
_session: &mut Session,
|
_session: &mut Session,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
if let Some(bridge) = &self.pty_bridge {
|
match &self.backend {
|
||||||
|
Some(SessionBackend::Local(bridge)) => {
|
||||||
let mut b = bridge.lock().await;
|
let mut b = bridge.lock().await;
|
||||||
let _ = b.writer.write_all(data);
|
let _ = b.writer.write_all(data);
|
||||||
let _ = b.writer.flush();
|
let _ = b.writer.flush();
|
||||||
}
|
}
|
||||||
|
Some(SessionBackend::Proxy(upstream)) => {
|
||||||
|
let u = upstream.lock().await;
|
||||||
|
let _ = u.handle.data(u.channel_id, CryptoVec::from_slice(data)).await;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub mod auth;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod hooks;
|
pub mod hooks;
|
||||||
|
pub mod proxy;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
|
||||||
152
crates/bascule-core/src/proxy.rs
Normal file
152
crates/bascule-core/src/proxy.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
//! Remote SSH proxy — bridges a client session to a target SSH host.
|
||||||
|
//!
|
||||||
|
//! When proxy mode is configured, Bascule authenticates the client,
|
||||||
|
//! then opens a second SSH connection to the target host and bridges
|
||||||
|
//! I/O between the two channels.
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! SSH client ←→ bascule (auth + hooks) ←→ target SSH host
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use russh::client;
|
||||||
|
use russh::ChannelMsg;
|
||||||
|
use russh_keys::key;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::config::ProxyConfig;
|
||||||
|
|
||||||
|
/// Minimal client handler for the upstream (target) SSH connection.
|
||||||
|
/// Accepts the target host key based on config.
|
||||||
|
pub struct UpstreamHandler {
|
||||||
|
accept_host_key: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpstreamHandler {
|
||||||
|
pub fn new(accept_host_key: bool) -> Self {
|
||||||
|
Self { accept_host_key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl client::Handler for UpstreamHandler {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
async fn check_server_key(
|
||||||
|
&mut self,
|
||||||
|
_server_public_key: &key::PublicKey,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
if self.accept_host_key {
|
||||||
|
tracing::warn!("Accepting target host key without verification (dev mode)");
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
// In production, validate against known_hosts or a key store.
|
||||||
|
// For now, reject unknown keys.
|
||||||
|
tracing::error!("Target host key rejected — set accept_target_host_key=true for dev");
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An established upstream SSH session with a channel ready for I/O.
|
||||||
|
pub struct UpstreamSession {
|
||||||
|
pub handle: client::Handle<UpstreamHandler>,
|
||||||
|
pub channel: Option<russh::Channel<client::Msg>>,
|
||||||
|
pub channel_id: russh::ChannelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to the target SSH host and authenticate.
|
||||||
|
pub async fn connect_upstream(
|
||||||
|
proxy_config: &ProxyConfig,
|
||||||
|
username: &str,
|
||||||
|
) -> anyhow::Result<UpstreamSession> {
|
||||||
|
let target_user = proxy_config
|
||||||
|
.target_user
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(username);
|
||||||
|
|
||||||
|
let addr = format!("{}:{}", proxy_config.target_host, proxy_config.target_port);
|
||||||
|
tracing::info!(target = %addr, user = %target_user, "Connecting to upstream SSH host");
|
||||||
|
|
||||||
|
let config = client::Config::default();
|
||||||
|
let handler = UpstreamHandler::new(proxy_config.accept_target_host_key);
|
||||||
|
|
||||||
|
let mut handle = russh::client::connect(Arc::new(config), &addr, handler).await?;
|
||||||
|
|
||||||
|
// Authenticate with the target host
|
||||||
|
if let Some(key_path) = &proxy_config.target_key_path {
|
||||||
|
let key = russh_keys::load_secret_key(key_path, None)?;
|
||||||
|
let authed = handle
|
||||||
|
.authenticate_publickey(target_user, Arc::new(key))
|
||||||
|
.await?;
|
||||||
|
if !authed {
|
||||||
|
anyhow::bail!("Public key authentication failed on target host");
|
||||||
|
}
|
||||||
|
tracing::info!("Authenticated to upstream via public key");
|
||||||
|
} else {
|
||||||
|
// Try none auth (some hosts allow it for certain users)
|
||||||
|
let authed = handle.authenticate_none(target_user).await?;
|
||||||
|
if !authed {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Cannot authenticate to target host — no target_key_path configured \
|
||||||
|
and none-auth rejected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = handle.channel_open_session().await?;
|
||||||
|
tracing::info!("Upstream channel opened");
|
||||||
|
|
||||||
|
let channel_id = channel.id();
|
||||||
|
Ok(UpstreamSession { handle, channel: Some(channel), channel_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridge I/O between the server-side SSH channel and the upstream client channel.
|
||||||
|
///
|
||||||
|
/// Reads from upstream → writes to client.
|
||||||
|
/// The reverse direction (client → upstream) is handled by the server Handler's
|
||||||
|
/// `data` method forwarding to the upstream channel writer.
|
||||||
|
pub async fn bridge_upstream_to_client(
|
||||||
|
mut upstream_channel: russh::Channel<client::Msg>,
|
||||||
|
server_handle: russh::server::Handle,
|
||||||
|
server_channel_id: russh::ChannelId,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
match upstream_channel.wait().await {
|
||||||
|
Some(ChannelMsg::Data { data }) => {
|
||||||
|
if server_handle
|
||||||
|
.data(server_channel_id, russh::CryptoVec::from_slice(&data))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ChannelMsg::ExtendedData { data, ext }) => {
|
||||||
|
if server_handle
|
||||||
|
.extended_data(server_channel_id, ext, russh::CryptoVec::from_slice(&data))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ChannelMsg::ExitStatus { exit_status }) => {
|
||||||
|
tracing::info!(exit_status, "Upstream session exited");
|
||||||
|
let _ = server_handle.exit_status_request(server_channel_id, exit_status).await;
|
||||||
|
let _ = server_handle.close(server_channel_id).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(ChannelMsg::Eof) => {
|
||||||
|
let _ = server_handle.eof(server_channel_id).await;
|
||||||
|
}
|
||||||
|
Some(ChannelMsg::Close) | None => {
|
||||||
|
let _ = server_handle.close(server_channel_id).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue