feat(smart-proxy): add UDP transport support with QUIC/HTTP3 routing and datagram handler relay
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! Backend connection pool for HTTP/1.1 and HTTP/2.
|
||||
//! Backend connection pool for HTTP/1.1, HTTP/2, and HTTP/3 (QUIC).
|
||||
//!
|
||||
//! Reuses idle keep-alive connections to avoid per-request TCP+TLS handshakes.
|
||||
//! HTTP/2 connections are multiplexed (clone the sender for each request).
|
||||
//! HTTP/2 and HTTP/3 connections are multiplexed (clone the sender / share the connection).
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -19,9 +19,17 @@ const IDLE_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
/// Background eviction interval.
|
||||
const EVICTION_INTERVAL: Duration = Duration::from_secs(30);
|
||||
/// Maximum age for pooled HTTP/2 connections before proactive eviction.
|
||||
/// Prevents staleness from backends that close idle connections (e.g. nginx GOAWAY).
|
||||
/// 120s is well within typical server GOAWAY windows (nginx: ~60s idle, envoy: ~60s).
|
||||
const MAX_H2_AGE: Duration = Duration::from_secs(120);
|
||||
/// Maximum age for pooled QUIC/HTTP/3 connections.
|
||||
const MAX_H3_AGE: Duration = Duration::from_secs(120);
|
||||
|
||||
/// Protocol for pool key discrimination.
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum PoolProtocol {
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
}
|
||||
|
||||
/// Identifies a unique backend endpoint.
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
@@ -29,7 +37,7 @@ pub struct PoolKey {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub use_tls: bool,
|
||||
pub h2: bool,
|
||||
pub protocol: PoolProtocol,
|
||||
}
|
||||
|
||||
/// An idle HTTP/1.1 sender with a timestamp for eviction.
|
||||
@@ -47,13 +55,22 @@ struct PooledH2 {
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
/// A pooled QUIC/HTTP/3 connection (multiplexed like H2).
|
||||
pub struct PooledH3 {
|
||||
pub connection: quinn::Connection,
|
||||
pub created_at: Instant,
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
/// Backend connection pool.
|
||||
pub struct ConnectionPool {
|
||||
/// HTTP/1.1 idle connections indexed by backend key.
|
||||
h1_pool: Arc<DashMap<PoolKey, Vec<IdleH1>>>,
|
||||
/// HTTP/2 multiplexed connections indexed by backend key.
|
||||
h2_pool: Arc<DashMap<PoolKey, PooledH2>>,
|
||||
/// Monotonic generation counter for H2 pool entries.
|
||||
/// HTTP/3 (QUIC) connections indexed by backend key.
|
||||
h3_pool: Arc<DashMap<PoolKey, PooledH3>>,
|
||||
/// Monotonic generation counter for H2/H3 pool entries.
|
||||
h2_generation: AtomicU64,
|
||||
/// Handle for the background eviction task.
|
||||
eviction_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
@@ -64,16 +81,19 @@ impl ConnectionPool {
|
||||
pub fn new() -> Self {
|
||||
let h1_pool: Arc<DashMap<PoolKey, Vec<IdleH1>>> = Arc::new(DashMap::new());
|
||||
let h2_pool: Arc<DashMap<PoolKey, PooledH2>> = Arc::new(DashMap::new());
|
||||
let h3_pool: Arc<DashMap<PoolKey, PooledH3>> = Arc::new(DashMap::new());
|
||||
|
||||
let h1_clone = Arc::clone(&h1_pool);
|
||||
let h2_clone = Arc::clone(&h2_pool);
|
||||
let h3_clone = Arc::clone(&h3_pool);
|
||||
let eviction_handle = tokio::spawn(async move {
|
||||
Self::eviction_loop(h1_clone, h2_clone).await;
|
||||
Self::eviction_loop(h1_clone, h2_clone, h3_clone).await;
|
||||
});
|
||||
|
||||
Self {
|
||||
h1_pool,
|
||||
h2_pool,
|
||||
h3_pool,
|
||||
h2_generation: AtomicU64::new(0),
|
||||
eviction_handle: Some(eviction_handle),
|
||||
}
|
||||
@@ -173,10 +193,57 @@ impl ConnectionPool {
|
||||
gen
|
||||
}
|
||||
|
||||
// ── HTTP/3 (QUIC) pool methods ──
|
||||
|
||||
/// Try to get a pooled QUIC connection for the given key.
|
||||
/// QUIC connections are multiplexed — the connection is shared, not removed.
|
||||
pub fn checkout_h3(&self, key: &PoolKey) -> Option<(quinn::Connection, Duration)> {
|
||||
let entry = self.h3_pool.get(key)?;
|
||||
let pooled = entry.value();
|
||||
let age = pooled.created_at.elapsed();
|
||||
|
||||
if age >= MAX_H3_AGE {
|
||||
drop(entry);
|
||||
self.h3_pool.remove(key);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if QUIC connection is still alive
|
||||
if pooled.connection.close_reason().is_some() {
|
||||
drop(entry);
|
||||
self.h3_pool.remove(key);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((pooled.connection.clone(), age))
|
||||
}
|
||||
|
||||
/// Register a QUIC connection in the pool. Returns the generation ID.
|
||||
pub fn register_h3(&self, key: PoolKey, connection: quinn::Connection) -> u64 {
|
||||
let gen = self.h2_generation.fetch_add(1, Ordering::Relaxed);
|
||||
self.h3_pool.insert(key, PooledH3 {
|
||||
connection,
|
||||
created_at: Instant::now(),
|
||||
generation: gen,
|
||||
});
|
||||
gen
|
||||
}
|
||||
|
||||
/// Remove a QUIC connection only if generation matches.
|
||||
pub fn remove_h3_if_generation(&self, key: &PoolKey, expected_gen: u64) {
|
||||
if let Some(entry) = self.h3_pool.get(key) {
|
||||
if entry.value().generation == expected_gen {
|
||||
drop(entry);
|
||||
self.h3_pool.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background eviction loop — runs every EVICTION_INTERVAL to remove stale connections.
|
||||
async fn eviction_loop(
|
||||
h1_pool: Arc<DashMap<PoolKey, Vec<IdleH1>>>,
|
||||
h2_pool: Arc<DashMap<PoolKey, PooledH2>>,
|
||||
h3_pool: Arc<DashMap<PoolKey, PooledH3>>,
|
||||
) {
|
||||
let mut interval = tokio::time::interval(EVICTION_INTERVAL);
|
||||
loop {
|
||||
@@ -206,6 +273,19 @@ impl ConnectionPool {
|
||||
for key in dead_h2 {
|
||||
h2_pool.remove(&key);
|
||||
}
|
||||
|
||||
// Evict dead or aged-out H3 (QUIC) connections
|
||||
let mut dead_h3 = Vec::new();
|
||||
for entry in h3_pool.iter() {
|
||||
if entry.value().connection.close_reason().is_some()
|
||||
|| entry.value().created_at.elapsed() >= MAX_H3_AGE
|
||||
{
|
||||
dead_h3.push(entry.key().clone());
|
||||
}
|
||||
}
|
||||
for key in dead_h3 {
|
||||
h3_pool.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
288
rust/crates/rustproxy-http/src/h3_service.rs
Normal file
288
rust/crates/rustproxy-http/src/h3_service.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
//! HTTP/3 proxy service.
|
||||
//!
|
||||
//! Accepts QUIC connections via quinn, runs h3 server to handle HTTP/3 requests,
|
||||
//! and forwards them to backends using the same routing and pool infrastructure
|
||||
//! as the HTTP/1+2 proxy.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use bytes::{Buf, Bytes};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use rustproxy_config::{RouteConfig, TransportProtocol};
|
||||
use rustproxy_metrics::MetricsCollector;
|
||||
use rustproxy_routing::{MatchContext, RouteManager};
|
||||
|
||||
use crate::connection_pool::ConnectionPool;
|
||||
use crate::protocol_cache::ProtocolCache;
|
||||
use crate::upstream_selector::UpstreamSelector;
|
||||
|
||||
/// HTTP/3 proxy service.
|
||||
///
|
||||
/// Handles QUIC connections with the h3 crate, parses HTTP/3 requests,
|
||||
/// and forwards them to backends using per-request route matching and
|
||||
/// shared connection pooling.
|
||||
pub struct H3ProxyService {
|
||||
route_manager: Arc<ArcSwap<RouteManager>>,
|
||||
metrics: Arc<MetricsCollector>,
|
||||
connection_pool: Arc<ConnectionPool>,
|
||||
#[allow(dead_code)]
|
||||
protocol_cache: Arc<ProtocolCache>,
|
||||
#[allow(dead_code)]
|
||||
upstream_selector: UpstreamSelector,
|
||||
#[allow(dead_code)]
|
||||
backend_tls_config: Arc<rustls::ClientConfig>,
|
||||
connect_timeout: Duration,
|
||||
}
|
||||
|
||||
impl H3ProxyService {
|
||||
pub fn new(
|
||||
route_manager: Arc<ArcSwap<RouteManager>>,
|
||||
metrics: Arc<MetricsCollector>,
|
||||
connection_pool: Arc<ConnectionPool>,
|
||||
protocol_cache: Arc<ProtocolCache>,
|
||||
backend_tls_config: Arc<rustls::ClientConfig>,
|
||||
connect_timeout: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
route_manager: Arc::clone(&route_manager),
|
||||
metrics: Arc::clone(&metrics),
|
||||
connection_pool,
|
||||
protocol_cache,
|
||||
upstream_selector: UpstreamSelector::new(),
|
||||
backend_tls_config,
|
||||
connect_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an accepted QUIC connection as HTTP/3.
|
||||
pub async fn handle_connection(
|
||||
&self,
|
||||
connection: quinn::Connection,
|
||||
_fallback_route: &RouteConfig,
|
||||
port: u16,
|
||||
) -> anyhow::Result<()> {
|
||||
let remote_addr = connection.remote_address();
|
||||
debug!("HTTP/3 connection from {} on port {}", remote_addr, port);
|
||||
|
||||
let mut h3_conn: h3::server::Connection<h3_quinn::Connection, Bytes> =
|
||||
h3::server::Connection::new(h3_quinn::Connection::new(connection))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("H3 connection setup failed: {}", e))?;
|
||||
|
||||
let client_ip = remote_addr.ip().to_string();
|
||||
|
||||
loop {
|
||||
match h3_conn.accept().await {
|
||||
Ok(Some(resolver)) => {
|
||||
let (request, stream) = match resolver.resolve_request().await {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
debug!("HTTP/3 request resolve error: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
self.metrics.record_http_request();
|
||||
|
||||
let rm = self.route_manager.load();
|
||||
let pool = Arc::clone(&self.connection_pool);
|
||||
let metrics = Arc::clone(&self.metrics);
|
||||
let connect_timeout = self.connect_timeout;
|
||||
let client_ip = client_ip.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_h3_request(
|
||||
request, stream, port, &client_ip, &rm, &pool, &metrics, connect_timeout,
|
||||
).await {
|
||||
debug!("HTTP/3 request error from {}: {}", client_ip, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("HTTP/3 connection from {} closed", remote_addr);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("HTTP/3 accept error from {}: {}", remote_addr, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single HTTP/3 request with per-request route matching.
|
||||
async fn handle_h3_request(
|
||||
request: hyper::Request<()>,
|
||||
mut stream: h3::server::RequestStream<h3_quinn::BidiStream<Bytes>, Bytes>,
|
||||
port: u16,
|
||||
client_ip: &str,
|
||||
route_manager: &RouteManager,
|
||||
_connection_pool: &ConnectionPool,
|
||||
metrics: &MetricsCollector,
|
||||
connect_timeout: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let method = request.method().clone();
|
||||
let uri = request.uri().clone();
|
||||
let path = uri.path().to_string();
|
||||
|
||||
// Extract host from :authority or Host header
|
||||
let host = request.uri().authority()
|
||||
.map(|a| a.as_str().to_string())
|
||||
.or_else(|| request.headers().get("host").and_then(|v| v.to_str().ok()).map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
|
||||
debug!("HTTP/3 {} {} (host: {}, client: {})", method, path, host, client_ip);
|
||||
|
||||
// Per-request route matching
|
||||
let ctx = MatchContext {
|
||||
port,
|
||||
domain: if host.is_empty() { None } else { Some(&host) },
|
||||
path: Some(&path),
|
||||
client_ip: Some(client_ip),
|
||||
tls_version: Some("TLSv1.3"),
|
||||
headers: None,
|
||||
is_tls: true,
|
||||
protocol: Some("http"),
|
||||
transport: Some(TransportProtocol::Udp),
|
||||
};
|
||||
|
||||
let route_match = route_manager.find_route(&ctx)
|
||||
.ok_or_else(|| anyhow::anyhow!("No route matched for HTTP/3 request to {}{}", host, path))?;
|
||||
let route = route_match.route;
|
||||
|
||||
// Resolve backend target (use matched target or first target)
|
||||
let target = route_match.target
|
||||
.or_else(|| route.action.targets.as_ref().and_then(|t| t.first()))
|
||||
.ok_or_else(|| anyhow::anyhow!("No target for HTTP/3 route"))?;
|
||||
|
||||
let backend_host = target.host.first();
|
||||
let backend_port = target.port.resolve(port);
|
||||
let backend_addr = format!("{}:{}", backend_host, backend_port);
|
||||
|
||||
// Read request body
|
||||
let mut body_data = Vec::new();
|
||||
while let Some(mut chunk) = stream.recv_data().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read H3 request body: {}", e))?
|
||||
{
|
||||
body_data.extend_from_slice(chunk.chunk());
|
||||
chunk.advance(chunk.remaining());
|
||||
}
|
||||
|
||||
// Connect to backend via TCP HTTP/1.1 with timeout
|
||||
let tcp_stream = tokio::time::timeout(
|
||||
connect_timeout,
|
||||
tokio::net::TcpStream::connect(&backend_addr),
|
||||
).await
|
||||
.map_err(|_| anyhow::anyhow!("Backend connect timeout to {}", backend_addr))?
|
||||
.map_err(|e| anyhow::anyhow!("Backend connect to {} failed: {}", backend_addr, e))?;
|
||||
|
||||
let _ = tcp_stream.set_nodelay(true);
|
||||
|
||||
let io = hyper_util::rt::TokioIo::new(tcp_stream);
|
||||
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend handshake failed: {}", e))?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = conn.await {
|
||||
debug!("Backend connection closed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let body = http_body_util::Full::new(Bytes::from(body_data));
|
||||
let backend_req = build_backend_request(&method, &backend_addr, &path, &host, &request, body)?;
|
||||
let response = sender.send_request(backend_req).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend request failed: {}", e))?;
|
||||
|
||||
// Build H3 response
|
||||
let status = response.status();
|
||||
let mut h3_response = hyper::Response::builder().status(status);
|
||||
|
||||
// Copy response headers (skip hop-by-hop)
|
||||
for (name, value) in response.headers() {
|
||||
let n = name.as_str().to_lowercase();
|
||||
if n == "transfer-encoding" || n == "connection" || n == "keep-alive" || n == "upgrade" {
|
||||
continue;
|
||||
}
|
||||
h3_response = h3_response.header(name, value);
|
||||
}
|
||||
|
||||
// Add Alt-Svc for HTTP/3 advertisement
|
||||
let alt_svc = route.action.udp.as_ref()
|
||||
.and_then(|u| u.quic.as_ref())
|
||||
.map(|q| {
|
||||
let p = q.alt_svc_port.unwrap_or(port);
|
||||
let ma = q.alt_svc_max_age.unwrap_or(86400);
|
||||
format!("h3=\":{}\"; ma={}", p, ma)
|
||||
})
|
||||
.unwrap_or_else(|| format!("h3=\":{}\"; ma=86400", port));
|
||||
h3_response = h3_response.header("alt-svc", alt_svc);
|
||||
|
||||
let h3_response = h3_response.body(())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to build H3 response: {}", e))?;
|
||||
|
||||
// Send response headers
|
||||
stream.send_response(h3_response).await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to send H3 response: {}", e))?;
|
||||
|
||||
// Stream response body back
|
||||
use http_body_util::BodyExt;
|
||||
let mut body = response.into_body();
|
||||
let mut total_bytes_out: u64 = 0;
|
||||
while let Some(frame) = body.frame().await {
|
||||
match frame {
|
||||
Ok(frame) => {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
total_bytes_out += data.len() as u64;
|
||||
stream.send_data(Bytes::copy_from_slice(data)).await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to send H3 data: {}", e))?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Backend body read error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
let route_id = route.name.as_deref().or(route.id.as_deref());
|
||||
metrics.record_bytes(0, total_bytes_out, route_id, Some(client_ip));
|
||||
|
||||
// Finish the stream
|
||||
stream.finish().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to finish H3 stream: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build an HTTP/1.1 backend request from the H3 frontend request.
|
||||
fn build_backend_request(
|
||||
method: &hyper::Method,
|
||||
backend_addr: &str,
|
||||
path: &str,
|
||||
host: &str,
|
||||
original_request: &hyper::Request<()>,
|
||||
body: http_body_util::Full<Bytes>,
|
||||
) -> anyhow::Result<hyper::Request<http_body_util::Full<Bytes>>> {
|
||||
let mut req = hyper::Request::builder()
|
||||
.method(method)
|
||||
.uri(format!("http://{}{}", backend_addr, path))
|
||||
.header("host", host);
|
||||
|
||||
// Forward non-pseudo headers
|
||||
for (name, value) in original_request.headers() {
|
||||
let n = name.as_str();
|
||||
if !n.starts_with(':') && n != "host" {
|
||||
req = req.header(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
req.body(body)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to build backend request: {}", e))
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod response_filter;
|
||||
pub mod shutdown_on_drop;
|
||||
pub mod template;
|
||||
pub mod upstream_selector;
|
||||
pub mod h3_service;
|
||||
|
||||
pub use connection_pool::*;
|
||||
pub use counting_body::*;
|
||||
|
||||
@@ -26,6 +26,7 @@ const PROTOCOL_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||
pub enum DetectedProtocol {
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
}
|
||||
|
||||
/// Key for the protocol cache: (host, port, requested_host).
|
||||
|
||||
@@ -451,6 +451,7 @@ impl HttpProxyService {
|
||||
headers: headers.as_ref(),
|
||||
is_tls: false,
|
||||
protocol: Some("http"),
|
||||
transport: None,
|
||||
};
|
||||
|
||||
let route_match = match current_rm.find_route(&ctx) {
|
||||
@@ -647,6 +648,11 @@ impl HttpProxyService {
|
||||
let (use_h2, needs_alpn_probe) = match backend_protocol_mode {
|
||||
rustproxy_config::BackendProtocol::Http1 => (false, false),
|
||||
rustproxy_config::BackendProtocol::Http2 => (true, false),
|
||||
rustproxy_config::BackendProtocol::Http3 => {
|
||||
// HTTP/3 (QUIC) backend connections not yet implemented — fall back to H1
|
||||
warn!("backendProtocol 'http3' not yet implemented, falling back to http1");
|
||||
(false, false)
|
||||
}
|
||||
rustproxy_config::BackendProtocol::Auto => {
|
||||
if !upstream.use_tls {
|
||||
// No ALPN without TLS — default to H1
|
||||
@@ -660,6 +666,10 @@ impl HttpProxyService {
|
||||
match self.protocol_cache.get(&cache_key) {
|
||||
Some(crate::protocol_cache::DetectedProtocol::H2) => (true, false),
|
||||
Some(crate::protocol_cache::DetectedProtocol::H1) => (false, false),
|
||||
Some(crate::protocol_cache::DetectedProtocol::H3) => {
|
||||
// H3 cached but we're on TCP — fall back to H2 probe
|
||||
(false, true)
|
||||
}
|
||||
None => (false, true), // needs ALPN probe
|
||||
}
|
||||
}
|
||||
@@ -673,7 +683,7 @@ impl HttpProxyService {
|
||||
host: upstream.host.clone(),
|
||||
port: upstream.port,
|
||||
use_tls: upstream.use_tls,
|
||||
h2: use_h2,
|
||||
protocol: if use_h2 { crate::connection_pool::PoolProtocol::H2 } else { crate::connection_pool::PoolProtocol::H1 },
|
||||
};
|
||||
|
||||
// H2 pool checkout — reuse pooled connections for all requests.
|
||||
@@ -832,7 +842,7 @@ impl HttpProxyService {
|
||||
host: upstream.host.clone(),
|
||||
port: upstream.port,
|
||||
use_tls: upstream.use_tls,
|
||||
h2: detected_h2,
|
||||
protocol: if detected_h2 { crate::connection_pool::PoolProtocol::H2 } else { crate::connection_pool::PoolProtocol::H1 },
|
||||
};
|
||||
|
||||
let io = TokioIo::new(backend);
|
||||
@@ -1298,7 +1308,7 @@ impl HttpProxyService {
|
||||
host: upstream.host.clone(),
|
||||
port: upstream.port,
|
||||
use_tls: upstream.use_tls,
|
||||
h2: false,
|
||||
protocol: crate::connection_pool::PoolProtocol::H1,
|
||||
};
|
||||
let fallback_io = TokioIo::new(fallback_backend);
|
||||
let result = self.forward_h1(
|
||||
@@ -1438,7 +1448,7 @@ impl HttpProxyService {
|
||||
host: upstream.host.clone(),
|
||||
port: upstream.port,
|
||||
use_tls: upstream.use_tls,
|
||||
h2: false,
|
||||
protocol: crate::connection_pool::PoolProtocol::H1,
|
||||
};
|
||||
let fallback_io = TokioIo::new(fallback_backend);
|
||||
let result = self.forward_h1(
|
||||
|
||||
@@ -10,7 +10,23 @@ pub struct ResponseFilter;
|
||||
impl ResponseFilter {
|
||||
/// Apply response headers from route config and CORS settings.
|
||||
/// If a `RequestContext` is provided, template variables in header values will be expanded.
|
||||
/// Also injects Alt-Svc header for routes with HTTP/3 enabled.
|
||||
pub fn apply_headers(route: &RouteConfig, headers: &mut HeaderMap, req_ctx: Option<&RequestContext>) {
|
||||
// Inject Alt-Svc for HTTP/3 advertisement if QUIC/HTTP3 is enabled on this route
|
||||
if let Some(ref udp) = route.action.udp {
|
||||
if let Some(ref quic) = udp.quic {
|
||||
if quic.enable_http3.unwrap_or(false) {
|
||||
let port = quic.alt_svc_port
|
||||
.or_else(|| req_ctx.map(|c| c.port))
|
||||
.unwrap_or(443);
|
||||
let max_age = quic.alt_svc_max_age.unwrap_or(86400);
|
||||
let alt_svc = format!("h3=\":{}\"; ma={}", port, max_age);
|
||||
if let Ok(val) = HeaderValue::from_str(&alt_svc) {
|
||||
headers.insert("alt-svc", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply custom response headers from route config
|
||||
if let Some(ref route_headers) = route.headers {
|
||||
if let Some(ref response_headers) = route_headers.response {
|
||||
|
||||
@@ -184,6 +184,7 @@ mod tests {
|
||||
send_proxy_protocol: None,
|
||||
headers: None,
|
||||
advanced: None,
|
||||
backend_transport: None,
|
||||
priority: None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user