v0.25.3
This commit is contained in:
1 parent
174389e5b1
commit
e6604e41fc
73 files changed
+1240
-201
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user