-
Notifications
You must be signed in to change notification settings - Fork 232
Add support for TLS passthrough routing #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Adds TLS passthrough routing with SNI-based listener selection, alongside existing TLS termination. Introduces a rewindable socket to peek ClientHello, adjusts API shapes to allow optional TLS config for TLS listeners, updates xDS conversion, and adds tests and example cert tooling.
- Add RewindSocket to buffer and replay ClientHello for SNI-based listener selection and optional TLS termination
- Change ListenerProtocol::TLS to accept Option (passthrough vs termination) and refactor LocalTLSServerConfig conversion via TryInto
- Extend tests (rewind, HTTPS termination) and example certs/script; log SNI when no Host header
Reviewed Changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/tls/certs/key.pem | Replace RSA key with EC private key for examples |
| examples/tls/certs/cert.pem | Replace example leaf certificate (ECDSA) |
| examples/tls/certs/ca-key.pem | Add example CA EC private key |
| examples/tls/certs/ca-cert.pem | Add example CA certificate |
| examples/tls/certs/gen.sh | Add script to generate example CA/leaf certs via step-cli |
| crates/agentgateway/src/types/local.rs | Make LocalTLSServerConfig public; implement TryInto; wire conversions in convert_listener |
| crates/agentgateway/src/types/agent_xds.rs | Support TLS passthrough in xDS conversion (Tls without config) |
| crates/agentgateway/src/types/agent.rs | Change ListenerProtocol::TLS to Option; adjust tls() helper |
| crates/agentgateway/src/transport/stream.rs | Add Rewind socket type and helpers; derive Default for TLSConnectionInfo; wire AsyncRead/Write variants |
| crates/agentgateway/src/transport/rewind.rs | New rewindable adapter buffering reads until rewind/discard; AsyncRead/Write impls |
| crates/agentgateway/src/transport/rewind_tests.rs | Tests for rewind and discard behaviors |
| crates/agentgateway/src/transport/mod.rs | Register rewind module |
| crates/agentgateway/src/test_helpers/proxymock.rs | MemoryConnector gains optional client TLS; add HTTPS client builder for tests |
| crates/agentgateway/src/telemetry/log.rs | Log tls.sni when Host is absent |
| crates/agentgateway/src/proxy/tcpproxy.rs | Cleanup unused variable; route selection uses SNI if available |
| crates/agentgateway/src/proxy/gateway_test.rs | Add HTTPS termination test validating SNI-based selection |
| crates/agentgateway/src/proxy/gateway.rs | Implement maybe_terminate_tls for SNI-based selection and passthrough/termination |
| crates/agentgateway/src/http/route.rs | Minor import tidy |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| impl tower::Service<Uri> for MemoryConnector { | ||
| type Response = TokioIo<Socket>; | ||
| type Error = Infallible; | ||
| type Future = Ready<Result<Self::Response, Self::Error>>; | ||
| type Error = crate::http::Error; | ||
| type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; | ||
|
|
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The associated type uses Future, but std::future::Future is not imported; this will fail to compile (cannot find type Future in this scope). Import it with use std::future::Future; or fully qualify as Pin<Box<dyn std::future::Future<...>>>.
| impl Socket {} | ||
|
|
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This empty impl block does nothing and should be removed to reduce noise.
| impl Socket {} |
| let listeners = inp.stores.read_binds().listeners(bind.clone()).unwrap(); | ||
| let (ext, counter, inner) = raw_stream.into_parts(); | ||
| let (mut ext, counter, inner) = raw_stream.into_parts(); | ||
| let inner = Socket::new_rewind(inner); | ||
| let acceptor = | ||
| tokio_rustls::LazyConfigAcceptor::new(rustls::server::Acceptor::default(), Box::new(inner)); | ||
| let start = acceptor.await?; | ||
| tokio_rustls::LazyConfigAcceptor::new(rustls::server::Acceptor::default(), inner); | ||
| let mut start = acceptor.await?; | ||
| let ch = start.client_hello(); | ||
| let sni = ch.server_name().unwrap_or_default(); | ||
| let best = listeners |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid unwrap() on listener lookup; it can panic if the bind key is missing. Return a structured error instead.
| ) -> anyhow::Result<(Arc<Listener>, Socket)> { | ||
| let to = inp.cfg.listener.tls_handshake_timeout; | ||
| let handshake = async move { | ||
| let listeners = inp.stores.read_binds().listeners(bind.clone()).unwrap(); |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested change: replace unwrap() with an error to prevent panics in edge cases.
| let listeners = inp.stores.read_binds().listeners(bind.clone()).unwrap(); | |
| let listeners = inp.stores.read_binds().listeners(bind.clone())?; |
| ) -> anyhow::Result<(Arc<Listener>, Socket)> { | ||
| let to = inp.cfg.listener.tls_handshake_timeout; | ||
| let handshake = async move { | ||
| let listeners = inp.stores.read_binds().listeners(bind.clone()).unwrap(); |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proposed fix:\n\nlet listeners = inp\n .stores\n .read_binds()\n .listeners(bind.clone())\n .ok_or_else(|| anyhow::anyhow!("no listeners for bind {bind}"))?;
| let listeners = inp.stores.read_binds().listeners(bind.clone()).unwrap(); | |
| let listeners = inp | |
| .stores | |
| .read_binds() | |
| .listeners(bind.clone()) | |
| .ok_or_else(|| anyhow!("no listeners for bind {bind}"))?; |
| /// HTTP | ||
| HTTP, | ||
| /// HTTPS, terminating TLS then treating as HTTP | ||
| HTTPS(TLSConfig), | ||
| TLS(TLSConfig), | ||
| /// TLS (passthrough or termination) | ||
| TLS(Option<TLSConfig>), | ||
| /// Opaque TCP | ||
| TCP, | ||
| HBONE, | ||
| } | ||
|
|
||
| impl ListenerProtocol { | ||
| pub fn tls(&self) -> Option<Arc<rustls::ServerConfig>> { | ||
| match self { | ||
| ListenerProtocol::HTTPS(t) | ListenerProtocol::TLS(t) => Some(t.config.clone()), | ||
| ListenerProtocol::HTTPS(t) => Some(t.config.clone()), | ||
| ListenerProtocol::TLS(t) => t.as_ref().map(|t| t.config.clone()), |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Changing TLS from TLS(TLSConfig) to TLS(Option<TLSConfig>) alters the serialized shape and is a breaking change for API consumers. Consider introducing a separate variant (e.g., TLSPassthrough) or maintaining the original variant for termination and adding a new one for passthrough to preserve compatibility; alternatively, document and version this change or add Serde attributes to minimize breakage.
| pub struct LocalTLSServerConfig { | ||
| pub cert: PathBuf, | ||
| pub key: PathBuf, |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Now that LocalTLSServerConfig is public, please add doc comments for the struct and fields (expected PEM formats, supported key types, and how relative paths are resolved) to guide users.
| pub struct LocalTLSServerConfig { | |
| pub cert: PathBuf, | |
| pub key: PathBuf, | |
| /// TLS server configuration for a local listener. | |
| /// | |
| /// The certificate and key files must be in PEM format. | |
| /// Supported key types: RSA and ECDSA. | |
| /// Relative paths are resolved relative to the working directory. | |
| pub struct LocalTLSServerConfig { | |
| /// Path to the PEM-encoded certificate file. | |
| /// This should contain the full certificate chain if needed. | |
| pub cert: PathBuf, | |
| /// Path to the PEM-encoded private key file. | |
| /// Supported key types: RSA and ECDSA. | |
| pub key: PathBuf, |
| @@ -0,0 +1,20 @@ | |||
| #!/bin/bash | |||
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add set -euo pipefail after the shebang to ensure the script fails fast on errors and unset variables.
| #!/bin/bash | |
| #!/bin/bash | |
| set -euo pipefail |
6272e81 to
7d9ad0f
Compare
7d9ad0f to
fda4685
Compare
No description provided.