Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33fdf42a70 | |||
| fb1c59ac9a | |||
| ea8224c400 | |||
| da1cc58a3d |
14
changelog.md
14
changelog.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-20 - 25.17.10 - fix(rustproxy-http)
|
||||
reuse the shared HTTP proxy service for HTTP/3 request handling
|
||||
|
||||
- Refactors H3ProxyService to delegate requests to the shared HttpProxyService instead of maintaining separate routing and backend forwarding logic.
|
||||
- Aligns HTTP/3 with the TCP/HTTP path for route matching, connection pooling, and ALPN-based upstream protocol detection.
|
||||
- Generalizes request handling and filters to accept boxed/generic HTTP bodies so both HTTP/3 and existing HTTP paths share the same proxy pipeline.
|
||||
- Updates the HTTP/3 integration route matcher to allow transport matching across shared HTTP and QUIC handling.
|
||||
|
||||
## 2026-03-20 - 25.17.9 - fix(rustproxy-http)
|
||||
correct HTTP/3 host extraction and avoid protocol filtering during UDP route lookup
|
||||
|
||||
- Use the URI host or strip the port from the Host header so HTTP/3 requests match routes consistently with TCP/HTTP handling.
|
||||
- Remove protocol filtering from HTTP/3 route lookup because QUIC transport already constrains routing to UDP and protocol validation happens earlier.
|
||||
|
||||
## 2026-03-20 - 25.17.8 - fix(rustproxy)
|
||||
use SNI-based certificate resolution for QUIC TLS connections
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "25.17.8",
|
||||
"version": "25.17.10",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
@@ -1,63 +1,36 @@
|
||||
//! 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.
|
||||
//! and delegates backend forwarding to the shared `HttpProxyService` — same
|
||||
//! route matching, connection pool, and protocol auto-detection as TCP/HTTP.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use bytes::{Buf, Bytes};
|
||||
use http_body::Frame;
|
||||
use http_body_util::BodyExt;
|
||||
use http_body_util::combinators::BoxBody;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use rustproxy_config::{RouteConfig, TransportProtocol};
|
||||
use rustproxy_metrics::MetricsCollector;
|
||||
use rustproxy_routing::{MatchContext, RouteManager};
|
||||
use rustproxy_config::RouteConfig;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::connection_pool::ConnectionPool;
|
||||
use crate::protocol_cache::ProtocolCache;
|
||||
use crate::upstream_selector::UpstreamSelector;
|
||||
use crate::proxy_service::{ConnActivity, HttpProxyService};
|
||||
|
||||
/// 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.
|
||||
/// Accepts QUIC connections, parses HTTP/3 requests, and delegates backend
|
||||
/// forwarding to the shared `HttpProxyService`.
|
||||
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,
|
||||
backend_tls_config: Arc<rustls::ClientConfig>,
|
||||
connect_timeout: Duration,
|
||||
http_proxy: Arc<HttpProxyService>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
pub fn new(http_proxy: Arc<HttpProxyService>) -> Self {
|
||||
Self { http_proxy }
|
||||
}
|
||||
|
||||
/// Handle an accepted QUIC connection as HTTP/3.
|
||||
@@ -81,8 +54,6 @@ impl H3ProxyService {
|
||||
.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)) => {
|
||||
@@ -94,21 +65,13 @@ impl H3ProxyService {
|
||||
}
|
||||
};
|
||||
|
||||
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 backend_tls = Arc::clone(&self.backend_tls_config);
|
||||
let connect_timeout = self.connect_timeout;
|
||||
let client_ip = client_ip.clone();
|
||||
let http_proxy = Arc::clone(&self.http_proxy);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_h3_request(
|
||||
request, stream, port, &client_ip, &rm, &pool, &metrics,
|
||||
&backend_tls, connect_timeout,
|
||||
request, stream, port, remote_addr, &http_proxy,
|
||||
).await {
|
||||
debug!("HTTP/3 request error from {}: {}", client_ip, e);
|
||||
debug!("HTTP/3 request error from {}: {}", remote_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -127,107 +90,27 @@ impl H3ProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single HTTP/3 request with per-request route matching.
|
||||
/// Handle a single HTTP/3 request by delegating to HttpProxyService.
|
||||
///
|
||||
/// 1. Read the H3 request body via an mpsc channel (streaming, not buffered)
|
||||
/// 2. Build a `hyper::Request<BoxBody>` that HttpProxyService can handle
|
||||
/// 3. Call `HttpProxyService::handle_request` — same route matching, connection
|
||||
/// pool, ALPN protocol detection (H1/H2/H3) as the TCP/HTTP path
|
||||
/// 4. Stream the response back over the H3 stream
|
||||
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,
|
||||
backend_tls_config: &Arc<rustls::ClientConfig>,
|
||||
connect_timeout: Duration,
|
||||
peer_addr: SocketAddr,
|
||||
http_proxy: &HttpProxyService,
|
||||
) -> 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);
|
||||
|
||||
// Determine if backend requires TLS (same logic as proxy_service.rs)
|
||||
let mut use_tls = target.tls.is_some();
|
||||
if let Some(ref tls) = route.action.tls {
|
||||
if tls.mode == rustproxy_config::TlsMode::TerminateAndReencrypt {
|
||||
use_tls = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to backend via TCP 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);
|
||||
|
||||
// Branch: wrap in TLS if backend requires it, then HTTP/1.1 handshake.
|
||||
// hyper's SendRequest<B> is NOT generic over the IO type, so both branches
|
||||
// produce the same type and can be unified.
|
||||
let mut sender = if use_tls {
|
||||
let connector = tokio_rustls::TlsConnector::from(Arc::clone(backend_tls_config));
|
||||
let server_name = rustls::pki_types::ServerName::try_from(backend_host.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid backend SNI '{}': {}", backend_host, e))?;
|
||||
let tls_stream = connector.connect(server_name, tcp_stream).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend TLS handshake to {} failed: {}", backend_addr, e))?;
|
||||
let io = hyper_util::rt::TokioIo::new(tls_stream);
|
||||
let (sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend handshake failed: {}", e))?;
|
||||
tokio::spawn(async move { let _ = conn.await; });
|
||||
sender
|
||||
} else {
|
||||
let io = hyper_util::rt::TokioIo::new(tcp_stream);
|
||||
let (sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend handshake failed: {}", e))?;
|
||||
tokio::spawn(async move { let _ = conn.await; });
|
||||
sender
|
||||
};
|
||||
|
||||
// Stream request body from H3 client to backend via an mpsc channel.
|
||||
// This avoids buffering the entire request body in memory.
|
||||
// Stream request body from H3 client via an mpsc channel.
|
||||
let (body_tx, body_rx) = tokio::sync::mpsc::channel::<Bytes>(4);
|
||||
let total_bytes_in = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let total_bytes_in_writer = Arc::clone(&total_bytes_in);
|
||||
|
||||
// Spawn the H3 body reader task
|
||||
let body_reader = tokio::spawn(async move {
|
||||
while let Ok(Some(mut chunk)) = stream.recv_data().await {
|
||||
let data = Bytes::copy_from_slice(chunk.chunk());
|
||||
total_bytes_in_writer.fetch_add(data.len() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
chunk.advance(chunk.remaining());
|
||||
if body_tx.send(data).await.is_err() {
|
||||
break;
|
||||
@@ -236,145 +119,64 @@ async fn handle_h3_request(
|
||||
stream
|
||||
});
|
||||
|
||||
// Create a body that polls from the mpsc receiver
|
||||
// Build a hyper::Request<BoxBody> from the H3 request + streaming body.
|
||||
// The URI already has scheme + authority + path set by the h3 crate.
|
||||
let body = H3RequestBody { receiver: body_rx };
|
||||
let backend_req = build_backend_request(&method, &backend_addr, &path, &host, &request, body, use_tls)?;
|
||||
let (parts, _) = request.into_parts();
|
||||
let boxed_body: BoxBody<Bytes, hyper::Error> = BoxBody::new(body);
|
||||
let req = hyper::Request::from_parts(parts, boxed_body);
|
||||
|
||||
let response = sender.send_request(backend_req).await
|
||||
// Delegate to HttpProxyService — same backend path as TCP/HTTP:
|
||||
// route matching, ALPN protocol detection, connection pool, H1/H2/H3 auto.
|
||||
let cancel = CancellationToken::new();
|
||||
let conn_activity = ConnActivity::new_standalone();
|
||||
let response = http_proxy.handle_request(req, peer_addr, port, cancel, conn_activity).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend request failed: {}", e))?;
|
||||
|
||||
// Await the body reader to get the stream back
|
||||
// Await the body reader to get the H3 stream back
|
||||
let mut stream = body_reader.await
|
||||
.map_err(|e| anyhow::anyhow!("Body reader task failed: {}", e))?;
|
||||
let total_bytes_in = total_bytes_in.load(std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// 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();
|
||||
// Send response headers over H3 (skip hop-by-hop headers)
|
||||
let (resp_parts, resp_body) = response.into_parts();
|
||||
let mut h3_response = hyper::Response::builder().status(resp_parts.status);
|
||||
for (name, value) in &resp_parts.headers {
|
||||
let n = name.as_str();
|
||||
if n == "transfer-encoding" || n == "connection" || n == "keep-alive" || n == "upgrade" {
|
||||
continue;
|
||||
}
|
||||
h3_response = h3_response.header(name, value);
|
||||
}
|
||||
|
||||
// Extract content-length for body loop termination (must be before into_body())
|
||||
let content_length: Option<u64> = response.headers()
|
||||
.get(hyper::header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
// 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;
|
||||
use http_body::Body as _;
|
||||
let mut body = response.into_body();
|
||||
let mut total_bytes_out: u64 = 0;
|
||||
|
||||
// Per-frame idle timeout: if no frame arrives within this duration, assume
|
||||
// the body is complete (or the backend has stalled). This prevents indefinite
|
||||
// hangs on close-delimited bodies or when hyper's internal trailers oneshot
|
||||
// never resolves after all data has been received.
|
||||
const FRAME_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
loop {
|
||||
// Layer 1: If the body already knows it is finished (Content-Length
|
||||
// bodies track remaining bytes internally), break immediately to
|
||||
// avoid blocking on hyper's internal trailers oneshot.
|
||||
if body.is_end_stream() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Layer 3: Per-frame idle timeout safety net
|
||||
match tokio::time::timeout(FRAME_IDLE_TIMEOUT, body.frame()).await {
|
||||
Ok(Some(Ok(frame))) => {
|
||||
// Stream response body back over H3
|
||||
let mut resp_body = resp_body;
|
||||
while let Some(frame) = resp_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))?;
|
||||
|
||||
// Layer 2: Content-Length byte count check
|
||||
if let Some(cl) = content_length {
|
||||
if total_bytes_out >= cl {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Err(e))) => {
|
||||
warn!("Backend body read error: {}", e);
|
||||
break;
|
||||
}
|
||||
Ok(None) => break, // Body ended naturally
|
||||
Err(_) => {
|
||||
debug!(
|
||||
"H3 body frame idle timeout ({:?}) after {} bytes; finishing stream",
|
||||
FRAME_IDLE_TIMEOUT, total_bytes_out
|
||||
);
|
||||
Err(e) => {
|
||||
warn!("Response body read error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
let route_id = route.name.as_deref().or(route.id.as_deref());
|
||||
metrics.record_bytes(total_bytes_in, total_bytes_out, route_id, Some(client_ip));
|
||||
|
||||
// Finish the stream
|
||||
// Finish the H3 stream (send QUIC FIN)
|
||||
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<B>(
|
||||
method: &hyper::Method,
|
||||
backend_addr: &str,
|
||||
path: &str,
|
||||
host: &str,
|
||||
original_request: &hyper::Request<()>,
|
||||
body: B,
|
||||
use_tls: bool,
|
||||
) -> anyhow::Result<hyper::Request<B>> {
|
||||
let scheme = if use_tls { "https" } else { "http" };
|
||||
let mut req = hyper::Request::builder()
|
||||
.method(method)
|
||||
.uri(format!("{}://{}{}", scheme, 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))
|
||||
}
|
||||
|
||||
/// A streaming request body backed by an mpsc channel receiver.
|
||||
///
|
||||
/// Implements `http_body::Body` so hyper can poll chunks as they arrive
|
||||
|
||||
@@ -36,7 +36,7 @@ use crate::upstream_selector::UpstreamSelector;
|
||||
/// Per-connection context for keeping the idle watchdog alive during body streaming.
|
||||
/// Passed through the forwarding chain so CountingBody can update the timestamp.
|
||||
#[derive(Clone)]
|
||||
struct ConnActivity {
|
||||
pub struct ConnActivity {
|
||||
last_activity: Arc<AtomicU64>,
|
||||
start: std::time::Instant,
|
||||
/// Active-request counter from handle_io's idle watchdog. When set, CountingBody
|
||||
@@ -49,6 +49,19 @@ struct ConnActivity {
|
||||
alt_svc_cache_key: Option<crate::protocol_cache::ProtocolCacheKey>,
|
||||
}
|
||||
|
||||
impl ConnActivity {
|
||||
/// Create a minimal ConnActivity (no idle watchdog, no Alt-Svc cache).
|
||||
/// Used by H3ProxyService where the TCP idle watchdog doesn't apply.
|
||||
pub fn new_standalone() -> Self {
|
||||
Self {
|
||||
last_activity: Arc::new(AtomicU64::new(0)),
|
||||
start: std::time::Instant::now(),
|
||||
active_requests: None,
|
||||
alt_svc_cache_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default upstream connect timeout (30 seconds).
|
||||
const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
@@ -347,6 +360,7 @@ impl HttpProxyService {
|
||||
let st = start;
|
||||
let ca = ConnActivity { last_activity: Arc::clone(&la_inner), start, active_requests: Some(Arc::clone(&ar_inner)), alt_svc_cache_key: None };
|
||||
async move {
|
||||
let req = req.map(|body| BoxBody::new(body));
|
||||
let result = svc.handle_request(req, peer, port, cn, ca).await;
|
||||
// Mark request end — update activity timestamp before guard drops
|
||||
la.store(st.elapsed().as_millis() as u64, Ordering::Relaxed);
|
||||
@@ -416,9 +430,13 @@ impl HttpProxyService {
|
||||
}
|
||||
|
||||
/// Handle a single HTTP request.
|
||||
async fn handle_request(
|
||||
///
|
||||
/// Accepts a generic body (`BoxBody`) so both the TCP/HTTP path (which boxes
|
||||
/// `Incoming`) and the H3 path (which boxes the H3 request body stream) can
|
||||
/// share the same backend forwarding logic.
|
||||
pub async fn handle_request(
|
||||
&self,
|
||||
req: Request<Incoming>,
|
||||
req: Request<BoxBody<Bytes, hyper::Error>>,
|
||||
peer_addr: std::net::SocketAddr,
|
||||
port: u16,
|
||||
cancel: CancellationToken,
|
||||
@@ -965,7 +983,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
io: TokioIo<BackendStream>,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
_upstream: &crate::upstream_selector::UpstreamSelection,
|
||||
@@ -1013,7 +1031,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
mut sender: hyper::client::conn::http1::SendRequest<BoxBody<Bytes, hyper::Error>>,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
route: &rustproxy_config::RouteConfig,
|
||||
@@ -1077,7 +1095,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
io: TokioIo<BackendStream>,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
_upstream: &crate::upstream_selector::UpstreamSelection,
|
||||
@@ -1151,7 +1169,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
sender: hyper::client::conn::http2::SendRequest<BoxBody<Bytes, hyper::Error>>,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
route: &rustproxy_config::RouteConfig,
|
||||
@@ -1344,7 +1362,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
io: TokioIo<BackendStream>,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
mut upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
upstream: &crate::upstream_selector::UpstreamSelection,
|
||||
@@ -1675,7 +1693,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
mut sender: hyper::client::conn::http2::SendRequest<BoxBody<Bytes, hyper::Error>>,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
route: &rustproxy_config::RouteConfig,
|
||||
@@ -1816,7 +1834,7 @@ impl HttpProxyService {
|
||||
/// Handle a WebSocket upgrade request (H1 Upgrade or H2 Extended CONNECT per RFC 8441).
|
||||
async fn handle_websocket_upgrade(
|
||||
&self,
|
||||
req: Request<Incoming>,
|
||||
req: Request<BoxBody<Bytes, hyper::Error>>,
|
||||
peer_addr: std::net::SocketAddr,
|
||||
upstream: &crate::upstream_selector::UpstreamSelection,
|
||||
route: &rustproxy_config::RouteConfig,
|
||||
@@ -2538,7 +2556,7 @@ impl HttpProxyService {
|
||||
&self,
|
||||
quic_conn: quinn::Connection,
|
||||
parts: hyper::http::request::Parts,
|
||||
body: Incoming,
|
||||
body: BoxBody<Bytes, hyper::Error>,
|
||||
upstream_headers: hyper::HeaderMap,
|
||||
upstream_path: &str,
|
||||
route: &rustproxy_config::RouteConfig,
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::sync::Arc;
|
||||
use bytes::Bytes;
|
||||
use http_body_util::Full;
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response, StatusCode};
|
||||
use http_body_util::combinators::BoxBody;
|
||||
|
||||
@@ -19,7 +18,7 @@ impl RequestFilter {
|
||||
/// Apply security filters. Returns Some(response) if the request should be blocked.
|
||||
pub fn apply(
|
||||
security: &RouteSecurity,
|
||||
req: &Request<Incoming>,
|
||||
req: &Request<impl hyper::body::Body>,
|
||||
peer_addr: &SocketAddr,
|
||||
) -> Option<Response<BoxBody<Bytes, hyper::Error>>> {
|
||||
Self::apply_with_rate_limiter(security, req, peer_addr, None)
|
||||
@@ -29,7 +28,7 @@ impl RequestFilter {
|
||||
/// Returns Some(response) if the request should be blocked.
|
||||
pub fn apply_with_rate_limiter(
|
||||
security: &RouteSecurity,
|
||||
req: &Request<Incoming>,
|
||||
req: &Request<impl hyper::body::Body>,
|
||||
peer_addr: &SocketAddr,
|
||||
rate_limiter: Option<&Arc<RateLimiter>>,
|
||||
) -> Option<Response<BoxBody<Bytes, hyper::Error>>> {
|
||||
@@ -182,7 +181,7 @@ impl RequestFilter {
|
||||
/// Determine the rate limit key based on configuration.
|
||||
fn rate_limit_key(
|
||||
config: &rustproxy_config::RouteRateLimit,
|
||||
req: &Request<Incoming>,
|
||||
req: &Request<impl hyper::body::Body>,
|
||||
peer_addr: &SocketAddr,
|
||||
) -> String {
|
||||
use rustproxy_config::RateLimitKeyBy;
|
||||
@@ -220,7 +219,7 @@ impl RequestFilter {
|
||||
/// Handle CORS preflight (OPTIONS) requests.
|
||||
/// Returns Some(response) if this is a CORS preflight that should be handled.
|
||||
pub fn handle_cors_preflight(
|
||||
req: &Request<Incoming>,
|
||||
req: &Request<impl hyper::body::Body>,
|
||||
) -> Option<Response<BoxBody<Bytes, hyper::Error>>> {
|
||||
if req.method() != hyper::Method::OPTIONS {
|
||||
return None;
|
||||
|
||||
@@ -428,6 +428,11 @@ impl TcpListenerManager {
|
||||
self.http_proxy.prune_stale_routes(active_route_ids);
|
||||
}
|
||||
|
||||
/// Get a reference to the HTTP proxy service (shared with H3).
|
||||
pub fn http_proxy(&self) -> &Arc<HttpProxyService> {
|
||||
&self.http_proxy
|
||||
}
|
||||
|
||||
/// Get a reference to the connection tracker.
|
||||
pub fn conn_tracker(&self) -> &Arc<ConnectionTracker> {
|
||||
&self.conn_tracker
|
||||
|
||||
@@ -343,15 +343,10 @@ impl RustProxy {
|
||||
);
|
||||
udp_mgr.set_proxy_ips(udp_proxy_ips.clone());
|
||||
|
||||
// Construct H3ProxyService for HTTP/3 request handling
|
||||
let h3_svc = rustproxy_http::h3_service::H3ProxyService::new(
|
||||
Arc::new(ArcSwap::from(Arc::clone(&*self.route_table.load()))),
|
||||
Arc::clone(&self.metrics),
|
||||
Arc::new(rustproxy_http::connection_pool::ConnectionPool::new()),
|
||||
Arc::new(rustproxy_http::protocol_cache::ProtocolCache::new()),
|
||||
rustproxy_passthrough::tls_handler::shared_backend_tls_config(),
|
||||
std::time::Duration::from_secs(30),
|
||||
);
|
||||
// Share HttpProxyService with H3 — same route matching, connection
|
||||
// pool, and ALPN protocol detection as the TCP/HTTP path.
|
||||
let http_proxy = self.listener_manager.as_ref().unwrap().http_proxy().clone();
|
||||
let h3_svc = rustproxy_http::h3_service::H3ProxyService::new(http_proxy);
|
||||
udp_mgr.set_h3_service(Arc::new(h3_svc));
|
||||
|
||||
for port in &udp_ports {
|
||||
|
||||
@@ -15,7 +15,7 @@ fn make_h3_route(
|
||||
key_pem: &str,
|
||||
) -> rustproxy_config::RouteConfig {
|
||||
let mut route = make_tls_terminate_route(port, "localhost", target_host, target_port, cert_pem, key_pem);
|
||||
route.route_match.transport = Some(TransportProtocol::Udp);
|
||||
route.route_match.transport = Some(TransportProtocol::All);
|
||||
// Keep domain="localhost" from make_tls_terminate_route — needed for TLS cert extraction
|
||||
route.action.udp = Some(RouteUdp {
|
||||
session_timeout: None,
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '25.17.8',
|
||||
version: '25.17.10',
|
||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user