/// Bridge an HTTP/2 Extended CONNECT stream to an already upgraded HTTP/1.1 /// backend WebSocket. DATA bytes remain byte-transparent in both directions. pub fn bridge_h2_to_h1( state: Arc, mut downstream: Body, upstream: Upgraded, exchange: Arc, upstream_label: String, lease: Option>, stop: Option, ) -> Body { let stop = stop .map(|value| value.0) .unwrap_or_else(|| state.shutdown.clone()); let session = state.tunnel_registry.register( exchange.clone(), upstream_label, "h2-rfc8441-h1-upstream", stop, lease, ); spawn_h2_idle_watch(state.clone(), &session); let upstream = TokioIo::new(upstream); let (upstream_read, mut upstream_write) = tokio::io::split(upstream); let request_session = session.clone(); let request_exchange = exchange.clone(); let tracker = state.tunnels.clone(); tracker.spawn(async move { let cancel = request_session.cancellation(); loop { let frame = tokio::select! { _ = cancel.cancelled() => return, frame = downstream.frame() => frame, }; match frame { Some(Ok(frame)) => { if let Ok(data) = frame.into_data() { if !data.is_empty() { if upstream_write.write_all(&data).await.is_err() { return; } record_session_bytes( &request_exchange, &request_session, data.len() as u64, true, ); } } } Some(Err(_)) => return, None => { let _ = upstream_write.shutdown().await; return; } } } }); let stream = futures_util::stream::unfold( (upstream_read, session, exchange), |(mut upstream_read, session, exchange)| async move { let mut buffer = vec![0u8; 16 * 1024]; let cancel = session.cancellation(); let read = tokio::select! { _ = cancel.cancelled() => return None, read = upstream_read.read(&mut buffer) => read, }; match read { Ok(0) => None, Ok(read) => { buffer.truncate(read); record_session_bytes(&exchange, &session, read as u64, false); Some(( Ok::(Bytes::from(buffer)), (upstream_read, session, exchange), )) } Err(error) => Some((Err(error), (upstream_read, session, exchange))), } }, ); Body::from_stream(stream) } fn benign_tunnel_close(error: &io::Error) -> bool { matches!( error.kind(), io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted | io::ErrorKind::BrokenPipe | io::ErrorKind::NotConnected ) } struct H3TunnelState { upstream: Upgraded, exchange: Arc, upstream_label: String, lease: Option>, stop: Option, } /// Response extension consumed by the HTTP/3 transport after it has sent the /// successful 200 response. The upstream connection is already upgraded before /// this extension is created, so downstream success never races backend takeover. #[derive(Clone)] pub struct H3Tunnel(Arc>>); impl H3Tunnel { pub fn new( upstream: Upgraded, exchange: Arc, upstream_label: String, lease: Option>, stop: Option, ) -> Self { Self(Arc::new(Mutex::new(Some(H3TunnelState { upstream, exchange, upstream_label, lease, stop, })))) } fn take(&self) -> Option { self.0.lock().unwrap_or_else(|e| e.into_inner()).take() } } fn h3_io_error(error: impl std::fmt::Display) -> io::Error { io::Error::other(error.to_string()) } async fn h3_client_to_backend( receive: &mut h3::server::RequestStream, upstream: &mut W, exchange: &Exchange, activity: &AtomicU64, started: Instant, session: &TunnelSession, ) -> io::Result<()> where W: AsyncWrite + Unpin, { while let Some(mut data) = receive.recv_data().await.map_err(h3_io_error)? { let bytes = data.copy_to_bytes(data.remaining()); if !bytes.is_empty() { upstream.write_all(&bytes).await?; record_bytes(exchange, bytes.len() as u64, true, activity, started); session.record(bytes.len() as u64, true); } } if receive .recv_trailers() .await .map_err(h3_io_error)? .is_some() { return Err(io::Error::other( "WebSocket Extended CONNECT must not end with HTTP trailers", )); } upstream.shutdown().await } async fn backend_to_h3( upstream: &mut R, send: &mut h3::server::RequestStream, Bytes>, exchange: &Exchange, activity: &AtomicU64, started: Instant, session: &TunnelSession, ) -> io::Result<()> where R: AsyncRead + Unpin, { let mut buffer = [0u8; 16 * 1024]; loop { let read = upstream.read(&mut buffer).await?; if read == 0 { send.finish().await.map_err(h3_io_error)?; return Ok(()); } send.send_data(Bytes::copy_from_slice(&buffer[..read])) .await .map_err(h3_io_error)?; record_bytes(exchange, read as u64, false, activity, started); session.record(read as u64, false); } } /// Bridge one RFC 9220 HTTP/3 stream to the already validated HTTP/1.1 /// WebSocket upgrade at the selected backend. DATA payload bytes are copied /// unchanged; FIN remains a half-close and abnormal termination maps to /// H3_REQUEST_CANCELLED. pub async fn run_h3( state: Arc, send: &mut h3::server::RequestStream, Bytes>, receive: &mut h3::server::RequestStream, tunnel: H3Tunnel, ) -> io::Result<()> { let Some(H3TunnelState { upstream, exchange, upstream_label, lease, stop, }) = tunnel.take() else { send.stop_stream(Code::H3_REQUEST_CANCELLED); receive.stop_sending(Code::H3_REQUEST_CANCELLED); return Err(io::Error::other("HTTP/3 WebSocket tunnel already consumed")); }; let stop = stop.map(|v| v.0).unwrap_or_else(|| state.shutdown.clone()); let started = Instant::now(); let activity = Arc::new(AtomicU64::new(0)); let session = state .tunnel_registry .register(exchange.clone(), upstream_label, "h3-rfc9220", stop, lease); let stop = session.cancellation(); let upstream = TokioIo::new(upstream); let (mut upstream_read, mut upstream_write) = tokio::io::split(upstream); let result = { let copying = async { tokio::try_join!( h3_client_to_backend( receive, &mut upstream_write, &exchange, &activity, started, &session ), backend_to_h3( &mut upstream_read, send, &exchange, &activity, started, &session ) )?; Ok::<(), io::Error>(()) }; tokio::pin!(copying); let mut remaining = state.websocket_idle_timeout; loop { tokio::select! { result = &mut copying => break result, _ = stop.cancelled() => break Err(io::Error::new(io::ErrorKind::ConnectionAborted, "WebSocket tunnel shutdown")), _ = tokio::time::sleep(remaining) => { let inactive = started.elapsed().saturating_sub(Duration::from_millis(activity.load(Ordering::Relaxed))); if inactive >= state.websocket_idle_timeout { break Err(io::Error::new(io::ErrorKind::TimedOut, "WebSocket idle timeout")); } remaining = state.websocket_idle_timeout - inactive; } } } }; if let Err(error) = &result { send.stop_stream(Code::H3_REQUEST_CANCELLED); receive.stop_sending(Code::H3_REQUEST_CANCELLED); exchange.h3_reset("H3_REQUEST_CANCELLED"); if !benign_tunnel_close(error) { exchange.failed.store(true, Ordering::Relaxed); exchange.websocket_error("tunnel_error", "h3_bridge_error"); state .metrics .websocket_errors .fetch_add(1, Ordering::Relaxed); } else { exchange.websocket_phase("tunnel_closed"); } } else { exchange.websocket_phase("tunnel_closed"); } result } #[allow(clippy::too_many_arguments)] pub fn spawn( state: Arc, downstream: OnUpgrade, upstream: OnUpgrade, exchange: Arc, upstream_label: String, lease: Option>, stop: Option, ) { let tracker = state.tunnels.clone(); tracker.spawn(async move { exchange.websocket_phase("upgrade_wait"); let stop = stop.map(|v| v.0).unwrap_or_else(|| state.shutdown.clone()); let upgraded = tokio::select! { _ = stop.cancelled() => return, result = tokio::time::timeout(Duration::from_secs(10), async { tokio::try_join!(downstream, upstream) }) => result, }; let (downstream, upstream) = match upgraded { Ok(Ok(streams)) => streams, _ => { exchange.failed.store(true, Ordering::Relaxed); exchange.websocket_error("upgrade_failed", "on_upgrade"); state .metrics .websocket_errors .fetch_add(1, Ordering::Relaxed); return; } }; let started = Instant::now(); let activity = Arc::new(AtomicU64::new(0)); let session = state.tunnel_registry.register( exchange.clone(), upstream_label, "h1-upgrade", stop, lease, ); let stop = session.cancellation(); let mut downstream = MeteredIo { inner: TokioIo::new(downstream), exchange: exchange.clone(), session: session.clone(), client_to_backend: true, started, activity: activity.clone(), }; let mut upstream = MeteredIo { inner: TokioIo::new(upstream), exchange: exchange.clone(), session: session.clone(), client_to_backend: false, started, activity: activity.clone(), }; if let Err(error) = tunnel( &mut downstream, &mut upstream, stop, activity, started, state.websocket_idle_timeout, ) .await { // Browsers and upstreams commonly close upgraded TCP streams without // a WebSocket close frame. Reset/EOF/broken-pipe are normal tunnel // termination signals, not malformed HTTP requests or proxy errors. if !benign_tunnel_close(&error) { exchange.failed.store(true, Ordering::Relaxed); exchange.websocket_error("tunnel_error", "bridge_error"); state .metrics .websocket_errors .fetch_add(1, Ordering::Relaxed); } else { exchange.websocket_phase("tunnel_closed"); } } else { exchange.websocket_phase("tunnel_closed"); } }); } async fn tunnel( a: &mut A, b: &mut B, stop: CancellationToken, activity: Arc, started: Instant, idle: Duration, ) -> io::Result<()> where A: AsyncRead + AsyncWrite + Unpin, B: AsyncRead + AsyncWrite + Unpin, { // Tokio uses fixed-size buffers in both directions. Writes apply backpressure. let copying = tokio::io::copy_bidirectional(a, b); tokio::pin!(copying); let mut remaining = idle; loop { tokio::select! { result = &mut copying => return result.map(|_| ()), _ = stop.cancelled() => return Ok(()), _ = tokio::time::sleep(remaining) => { let inactive = started.elapsed().saturating_sub(Duration::from_millis(activity.load(Ordering::Relaxed))); if inactive >= idle { return Err(io::Error::new(io::ErrorKind::TimedOut, "WebSocket idle timeout")); } remaining = idle - inactive; }, } } }