Files
codex/codex-rs/tcp-tunnel/src/control.rs
T
richardopenai 2aff7208fe Add a hidden HTTP/3 TCP tunnel command (#45900)
## What changed

Add `codex tcp-tunnel` and the `codex-tcp-tunnel` crate to forward loopback TCP connections to an explicit target through a TLS-verified HTTP/3 CONNECT proxy.

- Require the proxy origin to match an approved HTTPS origin in a supplied policy file.
- Read bearer tokens and optional bounded, non-forwarding `x-` headers from stdin. Support token updates for new connections, `LISTENING` and `AUTH_UPDATED` notifications, and shutdown when the control pipe closes in token-update mode.
- Preserve the listener across proxy reconnects without replaying TCP streams, and let accepted streams continue while a proxy drains.

## Testing

Add tests for hidden CLI parsing, proxy and target validation, credential renewal, control-pipe closure, and invalid input without secret disclosure. A local HTTP/3 proxy test covers token replacement, transport recovery without stream replay, and graceful draining.

GitOrigin-RevId: c6af3025301c61e9fe90940cf5a6df0039465e69
2026-09-16 09:03:41 +00:00

78 lines
2.9 KiB
Rust

//! Bounded parsing of HTTP/3 CONNECT metadata and bearer input.
//! Extension values remain sensitive and may never replace authorization or forwarding headers.
use std::io::BufRead;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::ensure;
use http::HeaderMap;
use http::HeaderValue;
use http::header;
pub(super) const MAX_TOKEN_BYTES: usize = 64 * 1024;
pub(super) const MAX_CONNECT_METADATA_BYTES: usize = 16 * 1024;
pub(super) const MAX_CONNECT_HEADERS: usize = 16;
pub(super) const MAX_CONNECT_HEADER_VALUE_BYTES: usize = 4 * 1024;
pub(super) fn read_connect_headers(reader: &mut impl BufRead) -> Result<HeaderMap> {
let mut line = Vec::new();
std::io::Read::take(&mut *reader, (MAX_CONNECT_METADATA_BYTES + 1) as u64)
.read_until(b'\n', &mut line)
.context("reading CONNECT metadata")?;
ensure!(
!line.is_empty() && line.len() <= MAX_CONNECT_METADATA_BYTES && line.ends_with(b"\n"),
"invalid or oversized CONNECT metadata"
);
let pairs: Vec<(String, String)> =
serde_json::from_slice(&line).map_err(|_| anyhow!("invalid CONNECT metadata"))?;
ensure!(
pairs.len() <= MAX_CONNECT_HEADERS,
"too many CONNECT metadata headers"
);
let mut headers = HeaderMap::new();
for (name, value) in pairs {
let name = header::HeaderName::from_bytes(name.as_bytes())
.map_err(|_| anyhow!("invalid CONNECT metadata header name"))?;
ensure!(
!headers.contains_key(&name),
"duplicate CONNECT metadata header"
);
let extension = name.as_str();
ensure!(
extension.starts_with("x-")
&& !extension.starts_with("x-forwarded-")
&& extension != "x-real-ip",
"CONNECT metadata must use non-forwarding extension headers"
);
let mut value = HeaderValue::from_str(&value)
.map_err(|_| anyhow!("invalid CONNECT metadata header value"))?;
ensure!(
value.as_bytes().len() <= MAX_CONNECT_HEADER_VALUE_BYTES,
"CONNECT metadata header value is too long"
);
value.set_sensitive(true);
headers.insert(name, value);
}
Ok(headers)
}
pub(super) fn read_auth_token(reader: &mut impl BufRead) -> Result<Option<HeaderValue>> {
let mut line = Vec::new();
std::io::Read::take(&mut *reader, (MAX_TOKEN_BYTES + 1) as u64)
.read_until(b'\n', &mut line)
.context("reading MASQUE token")?;
if line.is_empty() {
return Ok(None);
}
ensure!(line.len() <= MAX_TOKEN_BYTES, "MASQUE token is too long");
let token = std::str::from_utf8(&line)
.context("invalid MASQUE token encoding")?
.trim();
ensure!(!token.is_empty(), "empty MASQUE token");
let mut auth =
HeaderValue::from_str(&format!("Bearer {token}")).context("invalid MASQUE token header")?;
auth.set_sensitive(true);
Ok(Some(auth))
}