ok
This commit is contained in:
parent
c2070693fb
commit
909e241907
55 changed files with 594 additions and 223 deletions
|
|
@ -1,6 +1,6 @@
|
|||
use axum::body::Body;
|
||||
use axum::extract::{ConnectInfo, Request};
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use metrics::{counter, gauge, histogram};
|
||||
|
|
@ -119,7 +119,7 @@ fn normalize_path(path: &str) -> String {
|
|||
if path.starts_with("/assets/") {
|
||||
return "/assets/:file".to_string();
|
||||
}
|
||||
// Known application routes and API endpoints — keep as-is
|
||||
// Known application routes and API endpoints: keep as-is
|
||||
if path.starts_with("/api/")
|
||||
|| matches!(
|
||||
path,
|
||||
|
|
@ -147,11 +147,20 @@ fn normalize_path(path: &str) -> String {
|
|||
|
||||
/// Handler for the /metrics endpoint. Only accepts requests from peers on the
|
||||
/// same private network (loopback, RFC1918, or IPv6 unique/link-local).
|
||||
///
|
||||
/// The TCP peer is always the reverse proxy (a private container IP), so the
|
||||
/// real client IP is taken from the proxy-set `X-Real-IP` / `X-Forwarded-For`
|
||||
/// headers when present; a public client reaching this endpoint through the
|
||||
/// public domain is therefore rejected. Direct in-network connections (e.g. the
|
||||
/// Prometheus scraper hitting the container directly) carry no such header and
|
||||
/// fall back to the trusted TCP peer address.
|
||||
pub async fn metrics_handler(
|
||||
handle: PrometheusHandle,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(peer): ConnectInfo<SocketAddr>,
|
||||
) -> Response {
|
||||
if !is_same_network(peer.ip()) {
|
||||
let client_ip = client_ip_from_headers(&headers).unwrap_or_else(|| peer.ip());
|
||||
if !is_same_network(client_ip) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +176,27 @@ pub async fn metrics_handler(
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolve the real client IP from trusted reverse-proxy headers. nginx sets
|
||||
/// `X-Real-IP` to the post-`real_ip` client address and overwrites any
|
||||
/// client-supplied value, so when present it is authoritative; the left-most
|
||||
/// `X-Forwarded-For` hop is used as a fallback. Returns `None` when neither
|
||||
/// header is set, so a direct in-network connection uses its TCP peer address.
|
||||
fn client_ip_from_headers(headers: &HeaderMap) -> Option<IpAddr> {
|
||||
if let Some(value) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
|
||||
if let Ok(ip) = value.trim().parse::<IpAddr>() {
|
||||
return Some(ip);
|
||||
}
|
||||
}
|
||||
if let Some(value) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
|
||||
if let Some(first) = value.split(',').next() {
|
||||
if let Ok(ip) = first.trim().parse::<IpAddr>() {
|
||||
return Some(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_same_network(ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => v4.is_loopback() || v4.is_private() || v4.is_link_local(),
|
||||
|
|
@ -216,3 +246,34 @@ pub fn record_data_stats(property_count: usize, poi_count: usize, postcode_count
|
|||
gauge!("data_pois_loaded").set(poi_count as f64);
|
||||
gauge!("data_postcodes_loaded").set(postcode_count as f64);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn x_real_ip_is_authoritative_and_public_ip_is_rejected() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-real-ip", "203.0.113.7".parse().unwrap());
|
||||
let ip = client_ip_from_headers(&headers).expect("resolves from x-real-ip");
|
||||
assert_eq!(ip, "203.0.113.7".parse::<IpAddr>().unwrap());
|
||||
assert!(!is_same_network(ip), "a public client must be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_proxy_headers_fall_back_to_peer() {
|
||||
// No X-Real-IP / X-Forwarded-For → caller uses the TCP peer (e.g. an
|
||||
// in-network Prometheus scraper hitting the container directly).
|
||||
assert_eq!(client_ip_from_headers(&HeaderMap::new()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forwarded_for_uses_first_hop() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-forwarded-for", "198.51.100.4, 10.0.0.1".parse().unwrap());
|
||||
assert_eq!(
|
||||
client_ip_from_headers(&headers),
|
||||
Some("198.51.100.4".parse::<IpAddr>().unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue