This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 22:53:33 +02:00
1 parent 174389e5b1
commit e6604e41fc
73 files changed
+1240 -201

No files matched your search

+2
View File
@@ -29,6 +29,7 @@ pub struct Exchange {
pub transform_effective_enabled: Mutex<Option<bool>>,
pub backend: Mutex<Option<Arc<crate::balance::Lease>>>,
pub id: u64,
pub request_id: String,
pub method: String,
pub host: String,
pub path: String,
@@ -179,6 +180,7 @@ impl Drop for Exchange {
transform_effective_enabled,
backend,
id: self.id,
request_id: self.request_id.clone(),
time_ms: now_ms(),
method: self.method.clone(),
host: self.host.clone(),
+1
View File
@@ -15,6 +15,7 @@ mod tests {
transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None),
id: 1,
request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
method: "POST".into(),
host: "localhost".into(),
path: "/echo".into(),
+19 -2
View File
@@ -120,6 +120,23 @@ fn error(status: StatusCode, message: &'static str) -> Response {
response
}
fn method_policy_denial(
policy: &crate::vhost::MethodPolicy,
method: &Method,
) -> Option<Response> {
if policy.allows(method) {
return None;
}
let mut response = error(
StatusCode::METHOD_NOT_ALLOWED,
"RustProxy: HTTP method denied by access policy\n",
);
if let Some(value) = policy.allow_header() {
response.headers_mut().insert(http::header::ALLOW, value);
}
Some(response)
}
fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Method) -> Response {
let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body_allowed = !matches!(status.as_u16(), 100..=199 | 204 | 304) && *method != Method::HEAD;
@@ -211,7 +228,7 @@ fn finish_headers(
frontend: Option<&crate::config::CompiledFrontend>,
local: Option<std::net::SocketAddr>,
tls: bool,
id: u64,
request_id: &str,
active: &[crate::vhost::Listener],
) {
if let Some(frontend) = frontend {
@@ -224,7 +241,7 @@ fn finish_headers(
frontend
.map(|f| &f.diagnostic_headers)
.unwrap_or(&snapshot.envelope.config.diagnostic_headers)
.apply(response.headers_mut(), id);
.apply(response.headers_mut(), request_id);
// Authenticated content must not leak through an external shared cache either.
if frontend.is_some_and(|f| f.tinyauth.is_some() || f.basic_auth.is_some()) {
response.headers_mut().insert(
+4
View File
@@ -1,6 +1,10 @@
{
// Both scopes run before DNS, TCP, TLS or reuse of an existing upstream connection.
if !snapshot.host_allowed(&host, context.client_ip)
|| !route
.profile_acl
.as_ref()
.is_none_or(|acl| acl.allows(context.client_ip))
|| !route
.acl
.as_ref()
+52 -16
View File
@@ -141,6 +141,11 @@ async fn forward(
"RustProxy: RFC 9220 WebSocket transport is not enabled for this vhost\n",
);
}
if let Some(response) = frontend
.and_then(|frontend| method_policy_denial(&frontend.method_policy, request.method()))
{
return response;
}
if !tls {
if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) {
return response;
@@ -175,16 +180,19 @@ async fn forward(
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = Some(context.client_ip.to_string());
}
// GeoIP stays off the normal request path. Resolve lazily for any vhost or
// location policy that actually needs a country code, or for log capture.
let capture_country = state.logs.capture_country();
let mut country_resolved = false;
let mut country: Option<String> = None;
if let Some(frontend) = frontend {
// GeoIP is deliberately off the default request path. Resolve only when
// a country policy/rate-limit requires it or the operator explicitly
// enables country capture for access logs + traffic analytics.
let capture_country = state.logs.capture_country();
let country = (capture_country
if capture_country
|| frontend.profile_country_access.needs_geoip()
|| frontend.rate_limit.needs_geoip())
.then(|| state.geoip.country(context.client_ip))
.flatten();
|| frontend.rate_limit.needs_geoip()
{
country = state.geoip.country(context.client_ip);
country_resolved = true;
}
if capture_country {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
@@ -246,14 +254,42 @@ async fn forward(
return response;
}
let selected = snapshot.select_on(&host, &path, local.0, tls);
if !selected
.and_then(|r| r.acl.as_ref())
.is_none_or(|acl| acl.allows(context.client_ip))
{
return error(
StatusCode::FORBIDDEN,
"RustProxy: denied by access policy\n",
);
if let Some(route) = selected {
if let Some(response) = method_policy_denial(&route.method_policy, request.method()) {
return response;
}
if !route
.profile_acl
.as_ref()
.is_none_or(|acl| acl.allows(context.client_ip))
|| !route
.acl
.as_ref()
.is_none_or(|acl| acl.allows(context.client_ip))
{
return error(
StatusCode::FORBIDDEN,
"RustProxy: denied by access policy\n",
);
}
if (route.profile_country_access.needs_geoip() || route.country_access.needs_geoip())
&& !country_resolved
{
country = state.geoip.country(context.client_ip);
if capture_country {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
}
}
}
if !route.profile_country_access.allows(country.as_deref())
|| !route.country_access.allows(country.as_deref())
{
return error(
StatusCode::FORBIDDEN,
"RustProxy: location country access policy denied this request\n",
);
}
}
// The admin cookie is never ordinary application state. Strip it BEFORE
// auth/cache and keep only the removed fragment locally. A verified self-admin
+18 -3
View File
@@ -1,3 +1,16 @@
fn request_id_hash(sequence: u64) -> String {
use rand::RngCore;
use sha2::{Digest, Sha256};
let mut nonce = [0u8; 32];
let mut rng = rand::rngs::OsRng;
rng.fill_bytes(&mut nonce);
let mut hash = Sha256::new();
hash.update(sequence.to_be_bytes());
hash.update(nonce);
format!("{:x}", hash.finalize())
}
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
let snapshot = state.config.load_full();
let (frontend_context, route_context) = response_context(&snapshot, &request);
@@ -32,6 +45,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed);
}
let id = state.metrics.requests.fetch_add(1, Ordering::Relaxed) + 1;
let request_id = request_id_hash(id);
if let Some(frontend) = &frontend_context {
frontend.metrics.requests.fetch_add(1, Ordering::Relaxed);
}
@@ -70,7 +84,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
frontend_context.as_deref(),
local,
transport_tls,
id,
&request_id,
&state.active_listeners,
);
return response;
@@ -118,6 +132,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None),
id,
request_id: request_id.clone(),
method: request.method().to_string(),
host,
path: request.uri().path().chars().take(256).collect(),
@@ -148,7 +163,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
traffic: state.traffic.clone(),
_permit: permit,
});
if let Ok(value) = HeaderValue::from_str(&id.to_string()) {
if let Ok(value) = HeaderValue::from_str(&request_id) {
request.headers_mut().insert("x-request-id", value);
}
request.extensions_mut().insert(exchange.clone());
@@ -223,7 +238,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
frontend_context.as_deref(),
local,
transport_tls,
id,
&request_id,
&state.active_listeners,
);
if response.extensions().get::<RedirectResponse>().is_some_and(|redirect| !redirect.http3_enabled) {