feat(rustproxy): add protocol-based routing and backend TLS re-encryption support
This commit is contained in:
@@ -446,6 +446,7 @@ impl TcpListenerManager {
|
||||
tls_version: None,
|
||||
headers: None,
|
||||
is_tls: false,
|
||||
protocol: None,
|
||||
};
|
||||
|
||||
if let Some(quick_match) = route_manager.find_route(&quick_ctx) {
|
||||
@@ -650,6 +651,8 @@ impl TcpListenerManager {
|
||||
tls_version: None,
|
||||
headers: None,
|
||||
is_tls,
|
||||
// For TLS connections, protocol is unknown until after termination
|
||||
protocol: if is_http { Some("http") } else if !is_tls { Some("tcp") } else { None },
|
||||
};
|
||||
|
||||
let route_match = route_manager.find_route(&ctx);
|
||||
@@ -827,6 +830,15 @@ impl TcpListenerManager {
|
||||
}
|
||||
};
|
||||
|
||||
// Check protocol restriction from route config
|
||||
if let Some(ref required_protocol) = route_match.route.route_match.protocol {
|
||||
let detected = if peeked { "http" } else { "tcp" };
|
||||
if required_protocol != detected {
|
||||
debug!("Protocol mismatch: route requires '{}', got '{}'", required_protocol, detected);
|
||||
return Err("Protocol mismatch".into());
|
||||
}
|
||||
}
|
||||
|
||||
if peeked {
|
||||
debug!(
|
||||
"TLS Terminate + HTTP: {} -> {}:{} (domain: {:?})",
|
||||
@@ -867,12 +879,59 @@ impl TcpListenerManager {
|
||||
Ok(())
|
||||
}
|
||||
Some(rustproxy_config::TlsMode::TerminateAndReencrypt) => {
|
||||
// Inline TLS accept + HTTP detection (same pattern as Terminate mode)
|
||||
let route_tls = route_match.route.action.tls.as_ref();
|
||||
Self::handle_tls_terminate_reencrypt(
|
||||
stream, n, &domain, &target_host, target_port,
|
||||
peer_addr, &tls_configs, &shared_tls_acceptor,
|
||||
Arc::clone(&metrics), route_id, &conn_config, route_tls,
|
||||
).await
|
||||
let acceptor = Self::get_tls_acceptor(&domain, &tls_configs, &*shared_tls_acceptor, route_tls)?;
|
||||
let tls_stream = match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(conn_config.initial_data_timeout_ms),
|
||||
tls_handler::accept_tls(stream, &acceptor),
|
||||
).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => return Err("TLS handshake timeout".into()),
|
||||
};
|
||||
|
||||
// Peek at decrypted data to detect protocol
|
||||
let mut buf_stream = tokio::io::BufReader::new(tls_stream);
|
||||
let is_http_data = {
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
match buf_stream.fill_buf().await {
|
||||
Ok(data) => sni_parser::is_http(data),
|
||||
Err(_) => false,
|
||||
}
|
||||
};
|
||||
|
||||
// Check protocol restriction from route config
|
||||
if let Some(ref required_protocol) = route_match.route.route_match.protocol {
|
||||
let detected = if is_http_data { "http" } else { "tcp" };
|
||||
if required_protocol != detected {
|
||||
debug!("Protocol mismatch: route requires '{}', got '{}'", required_protocol, detected);
|
||||
return Err("Protocol mismatch".into());
|
||||
}
|
||||
}
|
||||
|
||||
if is_http_data {
|
||||
// HTTP: full per-request routing via HttpProxyService
|
||||
// (backend TLS handled by HttpProxyService when upstream.use_tls is set)
|
||||
debug!(
|
||||
"TLS Terminate+Reencrypt + HTTP: {} (domain: {:?})",
|
||||
peer_addr, domain
|
||||
);
|
||||
_conn_guard.disarm();
|
||||
http_proxy.handle_io(buf_stream, peer_addr, port, cancel.clone()).await;
|
||||
} else {
|
||||
// Non-HTTP: TLS-to-TLS tunnel (existing behavior for raw TCP protocols)
|
||||
debug!(
|
||||
"TLS Terminate+Reencrypt + TCP: {} -> {}:{}",
|
||||
peer_addr, target_host, target_port
|
||||
);
|
||||
Self::handle_tls_reencrypt_tunnel(
|
||||
buf_stream, &target_host, target_port,
|
||||
peer_addr, Arc::clone(&metrics), route_id,
|
||||
&conn_config,
|
||||
).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
if is_http {
|
||||
@@ -1007,39 +1066,18 @@ impl TcpListenerManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle TLS terminate-and-reencrypt: accept TLS from client, connect TLS to backend.
|
||||
async fn handle_tls_terminate_reencrypt(
|
||||
stream: tokio::net::TcpStream,
|
||||
_peek_len: usize,
|
||||
domain: &Option<String>,
|
||||
/// Handle non-HTTP TLS-to-TLS tunnel for terminate-and-reencrypt mode.
|
||||
/// TLS accept has already been done by the caller; this only connects to the
|
||||
/// backend over TLS and forwards bidirectionally.
|
||||
async fn handle_tls_reencrypt_tunnel(
|
||||
buf_stream: tokio::io::BufReader<tokio_rustls::server::TlsStream<tokio::net::TcpStream>>,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
peer_addr: std::net::SocketAddr,
|
||||
tls_configs: &HashMap<String, TlsCertConfig>,
|
||||
shared_tls_acceptor: &Option<TlsAcceptor>,
|
||||
metrics: Arc<MetricsCollector>,
|
||||
route_id: Option<&str>,
|
||||
conn_config: &ConnectionConfig,
|
||||
route_tls: Option<&rustproxy_config::RouteTls>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Use shared acceptor (session resumption) or fall back to per-connection
|
||||
let acceptor = Self::get_tls_acceptor(domain, tls_configs, shared_tls_acceptor, route_tls)?;
|
||||
|
||||
// Accept TLS from client with timeout
|
||||
let client_tls = match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(conn_config.initial_data_timeout_ms),
|
||||
tls_handler::accept_tls(stream, &acceptor),
|
||||
).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => return Err("TLS handshake timeout".into()),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"TLS Terminate+Reencrypt: {} -> {}:{} (domain: {:?})",
|
||||
peer_addr, target_host, target_port, domain
|
||||
);
|
||||
|
||||
// Connect to backend over TLS with timeout
|
||||
let backend_tls = match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(conn_config.connection_timeout_ms),
|
||||
@@ -1050,8 +1088,9 @@ impl TcpListenerManager {
|
||||
Err(_) => return Err("Backend TLS connection timeout".into()),
|
||||
};
|
||||
|
||||
// Forward between two TLS streams
|
||||
let (client_read, client_write) = tokio::io::split(client_tls);
|
||||
// Forward between decrypted client stream and backend TLS stream
|
||||
// (BufReader preserves any already-buffered data from the peek)
|
||||
let (client_read, client_write) = tokio::io::split(buf_stream);
|
||||
let (backend_read, backend_write) = tokio::io::split(backend_tls);
|
||||
|
||||
let base_inactivity_ms = conn_config.socket_timeout_ms;
|
||||
|
||||
Reference in New Issue
Block a user