This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 10:58:50 +02:00
1 parent c819f0a335
commit e1a59f6e56
53 files changed
+817 -380

No files matched your search

+9 -126
View File
@@ -1,23 +1,16 @@
//! Per-backend connection pools and connect-time DNS validation.
//! No HTTP caller can supply a destination. DNS answers are validated once and
//! passed directly to HttpConnector; there is no second unvalidated lookup.
use crate::config::{ip_policy, Backend, UpstreamProtocol};
use crate::config::{ip_policy, Backend};
use anyhow::{ensure, Context, Result};
use axum::body::Body;
use http::Uri;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use hyper_util::{
client::legacy::{
connect::{dns::Name, HttpConnector},
Client,
},
rt::{TokioExecutor, TokioTimer},
};
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::connect::{dns::Name, HttpConnector};
use ipnet::IpNet;
use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
pki_types::{CertificateDer, ServerName, UnixTime},
ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
DigitallySignedStruct, RootCertStore, SignatureScheme,
};
use std::{
future::Future,
@@ -36,7 +29,11 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const MAX_DNS_ANSWERS: usize = 32;
static DNS_SLOTS: OnceLock<Arc<Semaphore>> = OnceLock::new();
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub type ProxyClient = Client<TimedConnector, Body>;
mod client;
pub use client::{
client, client_with_keepalive, client_with_timeout, ClientRotationStats, ProxyClient,
};
#[derive(Clone, Debug, Default)]
pub struct AddressPolicy {
@@ -297,120 +294,6 @@ impl ServerCertVerifier for BackendCertificateVerificationDisabled {
self.supported_schemes.clone()
}
}
pub fn client(backend: &Backend, policy: AddressPolicy, websocket: bool) -> Result<ProxyClient> {
client_with_keepalive(
backend,
policy,
websocket,
&crate::balance::Keepalive::default(),
)
}
pub fn client_with_keepalive(
backend: &Backend,
policy: AddressPolicy,
websocket: bool,
keepalive: &crate::balance::Keepalive,
) -> Result<ProxyClient> {
client_with_timeout(backend, policy, websocket, keepalive, CONNECT_TIMEOUT)
}
pub fn client_with_timeout(
backend: &Backend,
policy: AddressPolicy,
websocket: bool,
keepalive: &crate::balance::Keepalive,
timeout: Duration,
) -> Result<ProxyClient> {
// HTTPS "http2" prefers h2 but always offers HTTP/1.1. Cleartext http2 remains
// explicit h2c prior knowledge; never replay a failed TLS request to downgrade.
let is_https = url::Url::parse(&backend.url)?.scheme() == "https";
let protocol = if websocket {
UpstreamProtocol::Http1
} else if backend.protocol == UpstreamProtocol::Http2 && is_https {
UpstreamProtocol::Auto
} else {
backend.protocol
};
let provider = Arc::new(rustls::crypto::ring::default_provider());
let trust_roots = if backend.tls_verify {
roots(backend)?
} else {
// tls_ca_pem is intentionally ignored when verification is disabled.
public_roots()
};
let mut config = ClientConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()?
.with_root_certificates(trust_roots)
.with_no_client_auth();
if !backend.tls_verify {
let supported_schemes = provider
.signature_verification_algorithms
.supported_schemes();
config.dangerous().set_certificate_verifier(Arc::new(
BackendCertificateVerificationDisabled { supported_schemes },
));
}
let connect_host = if backend.tls_server_name.is_some() {
Some(
url::Url::parse(&backend.url)?
.host_str()
.context("backend host missing")?
.trim_start_matches('[')
.trim_end_matches(']')
.to_owned(),
)
} else {
None
};
let mut http = HttpConnector::new_with_resolver(GuardedResolver {
policy: policy.clone(),
connect_host,
});
http.enforce_http(false);
http.set_connect_timeout(Some(timeout));
http.set_nodelay(true);
http.set_keepalive(
keepalive
.enabled
.then_some(Duration::from_secs(keepalive.tcp_keepalive_seconds)),
);
let builder = HttpsConnectorBuilder::new()
.with_tls_config(config)
.https_or_http();
let inner = match protocol {
UpstreamProtocol::Http1 => builder.enable_http1().wrap_connector(http),
UpstreamProtocol::Http2 => builder.enable_http2().wrap_connector(http),
UpstreamProtocol::Auto => builder.enable_http1().enable_http2().wrap_connector(http),
};
let mut builder = Client::builder(TokioExecutor::new());
builder
// Header field names are case-insensitive by HTTP specification, but some
// legacy/custom origins parse HTTP/1 request headers case-sensitively.
// Hyper writes lowercase names by default; title-case them on the wire for
// broad origin compatibility. This setting does not affect HTTP/2.
.http1_title_case_headers(true)
.pool_timer(TokioTimer::new())
.pool_idle_timeout(Duration::from_secs(keepalive.idle_timeout_seconds))
.pool_max_idle_per_host(if keepalive.enabled {
keepalive.max_idle_per_host
} else {
0
})
.retry_canceled_requests(false)
.http2_only(protocol == UpstreamProtocol::Http2)
.timer(TokioTimer::new())
.http2_keep_alive_interval(
keepalive
.enabled
.then_some(Duration::from_secs(keepalive.tcp_keepalive_seconds)),
)
.http2_keep_alive_timeout(Duration::from_secs(10))
.http2_keep_alive_while_idle(false);
Ok(builder.build(TimedConnector {
inner,
policy,
timeout,
}))
}
/// A bounded raw TCP liveness check, using the same configured-origin address
/// policy as normal requests. Every DNS answer is checked before any connection.
pub async fn tcp_probe(origin: &str, policy: &AddressPolicy) -> Result<()> {