Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/openshell-cli/src/edge_tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ async fn accept_loop(listener: TcpListener, config: Arc<TunnelConfig>) {
match listener.accept().await {
Ok((stream, peer)) => {
debug!(peer = %peer, "accepted local tunnel connection");
// Small gRPC frames must flush immediately; Nagle's algorithm
// otherwise adds per-write latency on this loopback hop.
if let Err(e) = stream.set_nodelay(true) {
debug!(peer = %peer, error = %e, "failed to set TCP_NODELAY");
}
let config = Arc::clone(&config);
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, &config).await {
Expand Down Expand Up @@ -174,6 +179,21 @@ async fn open_ws(config: &TunnelConfig) -> Result<WebSocketStream<MaybeTlsStream
.await
.map_err(|e| miette::miette!("WebSocket connect failed: {e}"))?;

// Disable Nagle's algorithm on the WebSocket's underlying TCP stream:
// tunneled gRPC frames are small and latency-sensitive.
let tcp = match ws_stream.get_ref() {
MaybeTlsStream::Plain(tcp) => Some(tcp),
MaybeTlsStream::Rustls(tls) => Some(tls.get_ref().0),
_ => None,
};
if let Some(tcp) = tcp {
if let Err(e) = tcp.set_nodelay(true) {
debug!(error = %e, "failed to set TCP_NODELAY on WebSocket stream");
}
} else {
debug!("unknown WebSocket stream variant; skipping TCP_NODELAY");
}

debug!(
status = %response.status(),
"WebSocket connected to edge"
Expand Down
5 changes: 5 additions & 0 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2891,6 +2891,11 @@ pub async fn service_forward_tcp(
let (socket, peer) = accepted
.into_diagnostic()
.wrap_err("failed to accept local forward connection")?;
// Forwarded request/response bytes are relayed in small frames;
// Nagle's algorithm would add per-write latency.
if let Err(err) = socket.set_nodelay(true) {
tracing::debug!(peer = %peer, error = %err, "failed to set TCP_NODELAY");
}
let mut client = client.clone();
let sandbox_id = sandbox_id.clone();
let target_host = target_host.to_string();
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-cli/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ impl tower::Service<hyper::Uri> for InsecureTlsConnector {
let port = uri.port_u16().unwrap_or(443);
let addr = format!("{host}:{port}");
let tcp = tokio::net::TcpStream::connect(addr).await?;
// Match tonic's default connector behavior: disable Nagle's
// algorithm so small gRPC frames are not delayed.
tcp.set_nodelay(true)?;
let server_name = ServerName::try_from(host)?;
let tls_stream = tls_connector.connect(server_name, tcp).await?;
Ok(hyper_util::rt::TokioIo::new(tls_stream))
Expand Down
6 changes: 6 additions & 0 deletions crates/openshell-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,12 @@ async fn serve_gateway_listener(
}
};

// Disable Nagle's algorithm: gateway responses and tunnel relay frames
// are small, latency-sensitive writes that must not wait for delayed ACKs.
if let Err(e) = stream.set_nodelay(true) {
debug!(error = %e, client = %addr, "Failed to set TCP_NODELAY on accepted connection");
}

spawn_gateway_connection(
stream,
addr,
Expand Down
46 changes: 33 additions & 13 deletions crates/openshell-supervisor-network/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ impl ProxyHandle {
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
// Proxied frames are relayed as they arrive; Nagle's
// algorithm would delay every small response.
if let Err(e) = stream.set_nodelay(true) {
tracing::debug!(error = %e, "failed to set TCP_NODELAY on accepted proxy connection");
}
let opa = opa_engine.clone();
let cache = identity_cache.clone();
let spid = entrypoint_pid.clone();
Expand Down Expand Up @@ -771,9 +776,7 @@ async fn handle_tcp_connection(
// addresses (used by rootless Podman with pasta) are not hard-blocked.
// Cloud metadata IPs and control-plane ports are still rejected.
match resolve_and_check_trusted_gateway(&host, port, gw, sandbox_entrypoint_pid).await {
Ok(addrs) => TcpStream::connect(addrs.as_slice())
.await
.into_diagnostic()?,
Ok(addrs) => connect_upstream(addrs.as_slice()).await.into_diagnostic()?,
Err(reason) => {
{
let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
Expand Down Expand Up @@ -827,9 +830,7 @@ async fn handle_tcp_connection(
match resolve_and_check_allowed_ips(&host, port, &nets, sandbox_entrypoint_pid)
.await
{
Ok(addrs) => TcpStream::connect(addrs.as_slice())
.await
.into_diagnostic()?,
Ok(addrs) => connect_upstream(addrs.as_slice()).await.into_diagnostic()?,
Err(reason) => {
{
let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
Expand Down Expand Up @@ -929,9 +930,7 @@ async fn handle_tcp_connection(
// the resolved IP in allowed_ips. Always-blocked addresses and
// control-plane ports remain denied.
match resolve_and_check_declared_endpoint(&host, port, sandbox_entrypoint_pid).await {
Ok(addrs) => TcpStream::connect(addrs.as_slice())
.await
.into_diagnostic()?,
Ok(addrs) => connect_upstream(addrs.as_slice()).await.into_diagnostic()?,
Err(reason) => {
{
let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
Expand Down Expand Up @@ -981,9 +980,7 @@ async fn handle_tcp_connection(
} else {
// Default: reject all internal IPs (loopback, RFC 1918, link-local).
match resolve_and_reject_internal(&host, port, sandbox_entrypoint_pid).await {
Ok(addrs) => TcpStream::connect(addrs.as_slice())
.await
.into_diagnostic()?,
Ok(addrs) => connect_upstream(addrs.as_slice()).await.into_diagnostic()?,
Err(reason) => {
{
let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
Expand Down Expand Up @@ -2097,6 +2094,17 @@ async fn write_all(writer: &mut (impl tokio::io::AsyncWrite + Unpin), data: &[u8
Ok(())
}

/// Connect to an upstream address with `TCP_NODELAY` set.
///
/// Tunneled request/response bytes are relayed as they arrive; without
/// `TCP_NODELAY`, Nagle's algorithm holds back every sub-MSS write and adds
/// milliseconds of latency to each small request/response round trip.
async fn connect_upstream(addrs: &[SocketAddr]) -> std::io::Result<TcpStream> {
let stream = TcpStream::connect(addrs).await?;
stream.set_nodelay(true)?;
Ok(stream)
}

#[derive(Debug, Clone)]
struct L7ConfigSnapshot {
config: crate::l7::L7EndpointConfig,
Expand Down Expand Up @@ -4121,7 +4129,7 @@ async fn handle_forward_proxy(
}

// 6. Connect upstream
let mut upstream = match TcpStream::connect(addrs.as_slice()).await {
let mut upstream = match connect_upstream(addrs.as_slice()).await {
Ok(s) => s,
Err(e) => {
let event = HttpActivityBuilder::new(openshell_ocsf::ctx::ctx())
Expand Down Expand Up @@ -4379,6 +4387,18 @@ mod tests {
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

/// Regression test: upstream connections must disable Nagle's algorithm,
/// otherwise every small tunneled request waits on delayed ACKs and adds
/// milliseconds of latency per round trip.
#[tokio::test]
async fn connect_upstream_sets_tcp_nodelay() {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local addr");

let stream = connect_upstream(&[addr]).await.expect("connect");
assert!(stream.nodelay().expect("query TCP_NODELAY"));
}

fn websocket_l7_config(
protocol: crate::l7::L7Protocol,
websocket_credential_rewrite: bool,
Expand Down
23 changes: 22 additions & 1 deletion crates/openshell-supervisor-process/src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,18 @@ pub async fn connect_in_netns(
.await
.map_err(|_| std::io::Error::other("netns connect thread panicked"))??;
std_stream.set_nonblocking(true)?;
// Forwarded frames are small and latency-sensitive; without
// TCP_NODELAY, Nagle's algorithm delays every sub-MSS response.
std_stream.set_nodelay(true)?;
return tokio::net::TcpStream::from_std(std_stream);
}

#[cfg(not(target_os = "linux"))]
let _ = netns_fd;

tokio::net::TcpStream::connect(addr).await
let stream = tokio::net::TcpStream::connect(addr).await?;
stream.set_nodelay(true)?;
Ok(stream)
}

#[derive(Clone)]
Expand Down Expand Up @@ -1275,6 +1280,22 @@ mod tests {
use super::*;
use std::process::Stdio;

/// Regression test: direct-tcpip forwards must disable Nagle's algorithm,
/// otherwise every small response from a sandbox service waits on delayed
/// ACKs and adds milliseconds of latency per round trip.
#[tokio::test]
async fn connect_in_netns_sets_tcp_nodelay() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = listener.local_addr().expect("local addr");

let stream = connect_in_netns(&addr.to_string(), None)
.await
.expect("connect");
assert!(stream.nodelay().expect("query TCP_NODELAY"));
}

/// Verify that dropping the input sender (the operation `channel_eof`
/// performs) causes the stdin writer loop to exit and close the child's
/// stdin pipe. Without this, commands like `cat | tar xf -` used by
Expand Down
Loading