fix: wire channel proxy into handler, add client reconnection with backoff, fix ADR-006 violations

- handler.channel_open_direct_tcpip now proxies non-wraith channels via
  connect_outbound+proxy_channel instead of dropping them
- ClientSession.run() spawns reconnect monitor that detects handle closure,
  reconnects with exponential backoff (1s/2s/4s/8s/16s/30s cap),
  and re-registers remote port forwards
- Remove server-side logging of tunnel destinations (ADR-006 compliance)
- Remove debug-level logging of proxy targets in channel_proxy
This commit is contained in:
2026-06-02 20:22:13 +00:00
parent f057e868ce
commit e49aef05d3
3 changed files with 122 additions and 17 deletions

View File

@@ -306,6 +306,60 @@ impl<T: Transport> ClientSession<T> {
};
let mut wait_shutdown = self.shutdown_rx.clone();
let reconnect_handle = Arc::clone(&self.handle);
let reconnect_transport = Arc::clone(&self.transport);
let reconnect_auth = Arc::clone(&self.auth_config);
let reconnect_username = self.username.clone();
let reconnect_shutdown = self.shutdown_rx.clone();
let reconnect_remote_specs = remote_specs.clone();
let reconnect_monitor = tokio::spawn(async move {
let mut attempts: u32 = 0;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
if *reconnect_shutdown.borrow() {
break;
}
let h = reconnect_handle.lock().await;
if h.is_closed() {
drop(h);
info!("SSH session closed, starting reconnection");
let backoff = backoff_duration(attempts);
warn!("reconnect attempt #{}, waiting {:?}", attempts + 1, backoff);
tokio::time::sleep(backoff).await;
let handler = ClientHandler::from_config(&reconnect_auth);
let username = reconnect_username.clone();
match establish_session(&*reconnect_transport, handler, &reconnect_auth, &username).await {
Ok(new_handle) => {
info!("reconnection successful");
{
let mut guard = reconnect_handle.lock().await;
*guard = new_handle;
}
for spec in &reconnect_remote_specs {
match RemoteForwarder::new(spec.clone()) {
Ok(rf) => {
let mut h = reconnect_handle.lock().await;
match rf.register(&mut h).await {
Ok(_) => debug!("re-registered remote forward: {}", spec),
Err(e) => warn!("failed to re-register remote forward {}: {e}", spec),
}
}
Err(e) => warn!("failed to create remote forwarder: {e}"),
}
}
attempts = 0;
}
Err(e) => {
warn!("reconnection attempt failed: {e}");
attempts += 1;
}
}
}
}
});
tokio::select! {
_ = wait_shutdown.changed() => {
if *wait_shutdown.borrow() {
@@ -317,6 +371,8 @@ impl<T: Transport> ClientSession<T> {
}
}
reconnect_monitor.abort();
#[cfg(unix)]
signal_done.abort();
@@ -358,6 +414,48 @@ fn derive_username() -> String {
.unwrap_or_else(|_| "wraith".to_string())
}
async fn establish_session<T: Transport>(
transport: &T,
handler: ClientHandler,
auth_config: &ClientAuthConfig,
username: &str,
) -> Result<client::Handle<ClientHandler>, ConnectError> {
let stream = transport.connect().await.map_err(|e| {
error!("transport connect failed: {e}");
ConnectError::ConnectionFailed
})?;
let config = Arc::new(client::Config::default());
let mut handle = client::connect_stream(config, stream, handler)
.await
.map_err(|e| {
error!("SSH connect failed: {e}");
ConnectError::ConnectionFailed
})?;
let auth_ok = auth_config
.authenticate(&mut handle, username)
.await
.map_err(|_| ConnectError::AuthFailed)?;
if !auth_ok {
return Err(ConnectError::AuthFailed);
}
Ok(handle)
}
fn backoff_duration(attempt: u32) -> Duration {
let secs: u64 = match attempt {
0 => 1,
1 => 2,
2 => 4,
3 => 8,
4 => 16,
_ => 30,
};
Duration::from_secs(secs)
}
fn build_local_forwarders(opts: &ConnectOptions) -> Result<Vec<LocalForwarder>, ConnectError> {
let mut forwarders = Vec::new();
for spec_str in &opts.forwards {

View File

@@ -142,16 +142,13 @@ async fn connect_http_connect(
}
}
fn map_connection_error(e: std::io::Error, target: SocketAddr) -> ChannelProxyError {
fn map_connection_error(e: std::io::Error, _target: SocketAddr) -> ChannelProxyError {
match e.kind() {
std::io::ErrorKind::ConnectionRefused => ChannelProxyError::ConnectionRefused,
std::io::ErrorKind::AddrNotAvailable
| std::io::ErrorKind::NetworkUnreachable
| std::io::ErrorKind::HostUnreachable => ChannelProxyError::TargetUnreachable,
_ => {
tracing::debug!(error = %e, "outbound connection failed to {:?}", target);
ChannelProxyError::Io(e)
}
_ => ChannelProxyError::Io(e),
}
}

View File

@@ -210,17 +210,7 @@ impl Handler for ServerHandler {
_session: &mut Session,
) -> Result<bool, Self::Error> {
if host_to_connect.starts_with(WRAITH_PREFIX) {
tracing::info!(
host = host_to_connect,
port = port_to_connect,
"routing to internal control channel handler"
);
if !self.control_channel_router.has_handler() {
tracing::warn!(
host = host_to_connect,
"no control channel handler configured, rejecting channel open"
);
return Ok(false);
}
@@ -228,8 +218,28 @@ impl Handler for ServerHandler {
return Ok(true);
}
let _ = (host_to_connect, port_to_connect, originator_address, originator_port, channel);
Ok(false)
let target_host = host_to_connect.to_string();
let target_port = port_to_connect;
let proxy_config = self.outbound_proxy.clone().unwrap_or(ProxyConfig {
mode: ProxyMode::Direct,
});
tokio::spawn(async move {
let target = match format!("{target_host}:{target_port}").parse::<std::net::SocketAddr>() {
Ok(addr) => addr,
Err(_) => match tokio::net::lookup_host((&target_host[..], target_port as u16)).await {
Ok(mut addrs) => match addrs.next() {
Some(addr) => addr,
None => return,
},
Err(_) => return,
},
};
crate::server::channel_proxy::proxy_channel(channel.into_stream(), target, &proxy_config).await;
});
let _ = (originator_address, originator_port);
Ok(true)
}
async fn channel_open_session(