mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-16 05:20:22 +08:00
* fix(runtime): recover SSH relays and bound startup diagnostics Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): deliver pending relays once per supervisor session Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): satisfy relay delivery clippy diagnostics Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): bound relay setup with one absolute deadline Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <anewberry@nvidia.com>
118 lines
4.9 KiB
Rust
118 lines
4.9 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
use bytes::Bytes;
|
|
use http_body_util::{BodyExt, StreamBody};
|
|
use hyper::body::Frame;
|
|
use hyper::service::service_fn;
|
|
use hyper_util::rt::{TokioExecutor, TokioIo};
|
|
use openshell_core::proto::{TcpForwardFrame, tcp_forward_frame};
|
|
use prost::Message;
|
|
use std::convert::Infallible;
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
async fn proxy_exits_with_stdin_open(relay_status: tonic::Status) {
|
|
let expected_code = relay_status.code();
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let address = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (socket, _) = listener.accept().await.unwrap();
|
|
let service = service_fn(move |request: hyper::Request<hyper::body::Incoming>| {
|
|
let status = relay_status.clone();
|
|
async move {
|
|
assert_eq!(request.uri().path(), "/openshell.v1.OpenShell/ForwardTcp");
|
|
let body = StreamBody::new(futures::stream::once(async move {
|
|
// Wait until stdin actually traverses the proxy. After this
|
|
// chunk its reader blocks again, with the parent pipe open.
|
|
let mut inbound = request.into_body();
|
|
let mut pending = Vec::new();
|
|
'input: while let Some(frame) = inbound.frame().await {
|
|
if let Ok(data) = frame.unwrap().into_data() {
|
|
pending.extend_from_slice(&data);
|
|
while pending.len() >= 5 {
|
|
let length =
|
|
u32::from_be_bytes(pending[1..5].try_into().unwrap()) as usize;
|
|
if pending.len() < 5 + length {
|
|
break;
|
|
}
|
|
let frame =
|
|
TcpForwardFrame::decode(&pending[5..5 + length]).unwrap();
|
|
pending.drain(..5 + length);
|
|
if matches!(frame.payload, Some(tcp_forward_frame::Payload::Data(data)) if data == b"probe")
|
|
{
|
|
break 'input;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
let trailers = status.into_http::<()>().into_parts().0.headers;
|
|
Ok::<Frame<Bytes>, Infallible>(Frame::trailers(trailers))
|
|
}));
|
|
let mut response = hyper::Response::new(body);
|
|
response
|
|
.headers_mut()
|
|
.insert("content-type", "application/grpc".parse().unwrap());
|
|
Ok::<_, Infallible>(response)
|
|
}
|
|
});
|
|
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
|
|
.serve_connection(TokioIo::new(socket), service)
|
|
.await;
|
|
});
|
|
let config = tempfile::tempdir().unwrap();
|
|
let mut child = tokio::process::Command::new(env!("CARGO_BIN_EXE_openshell"))
|
|
.args([
|
|
"ssh-proxy",
|
|
"--gateway",
|
|
&format!("http://{address}/proxy/connect"),
|
|
"--sandbox",
|
|
"repro",
|
|
"--token",
|
|
"test-token",
|
|
])
|
|
.env("XDG_CONFIG_HOME", config.path())
|
|
.env("OPENSHELL_TELEMETRY_ENABLED", "false")
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::piped())
|
|
.kill_on_drop(true)
|
|
.spawn()
|
|
.unwrap();
|
|
child
|
|
.stdin
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(b"probe")
|
|
.await
|
|
.unwrap();
|
|
// Keep child.stdin alive throughout wait: closing it would conceal the bug.
|
|
let status = tokio::time::timeout(Duration::from_secs(5), child.wait())
|
|
.await
|
|
.expect("SSH proxy must exit even while the parent holds stdin open")
|
|
.unwrap();
|
|
let mut stderr = String::new();
|
|
tokio::io::AsyncReadExt::read_to_string(child.stderr.as_mut().unwrap(), &mut stderr)
|
|
.await
|
|
.unwrap();
|
|
if expected_code == tonic::Code::Ok {
|
|
assert!(status.success(), "{stderr}");
|
|
} else {
|
|
assert!(!status.success(), "relay failure must propagate to SSH");
|
|
assert!(stderr.contains("Deadline expired"), "{stderr}");
|
|
}
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn proxy_exits_after_relay_error_with_stdin_open() {
|
|
proxy_exits_with_stdin_open(tonic::Status::deadline_exceeded("relay open timed out")).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn proxy_exits_after_clean_relay_close_with_stdin_open() {
|
|
proxy_exits_with_stdin_open(tonic::Status::ok("")).await;
|
|
}
|