Skip to main content

aster_forge_middleware/axum/
client_ip.rs

1//! Trusted-proxy-aware HTTP header adapter. Direct peers come from the connection, never headers.
2use axum::extract::{ConnectInfo, Request};
3use http::HeaderMap;
4use ipnet::IpNet;
5use std::net::{IpAddr, SocketAddr};
6
7/// Resolves a client IP using raw trusted CIDRs/IPs and an explicit direct peer.
8#[must_use]
9pub fn real_ip_from_headers(
10    headers: &HeaderMap,
11    peer: IpAddr,
12    trusted_proxies: &[String],
13) -> IpAddr {
14    real_ip_from_trusted_headers(
15        headers,
16        peer,
17        &aster_forge_utils::net::parse_trusted_proxies(trusted_proxies),
18    )
19}
20
21/// Resolves a client IP using parsed trusted proxies. Invalid forwarded values fall back to peer.
22#[must_use]
23pub fn real_ip_from_trusted_headers(
24    headers: &HeaderMap,
25    peer: IpAddr,
26    trusted: &[IpNet],
27) -> IpAddr {
28    let forwarded = headers
29        .get("x-forwarded-for")
30        .and_then(|value| value.to_str().ok());
31    aster_forge_utils::net::real_ip_from_forwarded_for(forwarded, peer, trusted)
32}
33
34/// Reads the TCP peer populated by `into_make_service_with_connect_info::<SocketAddr>()`.
35/// UDS or services without connection info return `None`; headers cannot fill this gap.
36#[must_use]
37pub fn direct_peer(request: &Request) -> Option<IpAddr> {
38    request
39        .extensions()
40        .get::<ConnectInfo<SocketAddr>>()
41        .map(|peer| peer.0.ip())
42}
43
44/// Resolves the request's client IP, retaining `None` when connection info is missing.
45#[must_use]
46pub fn client_ip(request: &Request, trusted: &[IpNet]) -> Option<IpAddr> {
47    direct_peer(request).map(|peer| real_ip_from_trusted_headers(request.headers(), peer, trusted))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use axum::{
54        Router,
55        body::{Body, to_bytes},
56        middleware,
57        response::IntoResponse,
58        routing::get,
59    };
60    use http::{HeaderValue, Request as HttpRequest};
61    use tower::ServiceExt;
62
63    #[tokio::test]
64    async fn router_resolves_only_connection_proven_trusted_peers() {
65        let trusted = aster_forge_utils::net::parse_trusted_proxies(&[
66            "10.0.0.0/8".into(),
67            "192.168.1.1".into(),
68            "2001:db8:ffff::/48".into(),
69            "127.0.0.1".into(),
70            "bad-entry".into(),
71        ]);
72        let router =
73            Router::new()
74                .route("/", get(|| async { "unused" }))
75                .layer(middleware::from_fn(
76                    move |request: Request, _next: axum::middleware::Next| {
77                        let trusted = trusted.clone();
78                        async move {
79                            client_ip(&request, &trusted)
80                                .map_or_else(|| "none".into(), |ip| ip.to_string())
81                                .into_response()
82                        }
83                    },
84                ));
85        for (peer, forwarded, expected) in [
86            (
87                Some("10.0.0.5:1234"),
88                "203.0.113.10, 198.51.100.2",
89                "203.0.113.10",
90            ),
91            (Some("192.168.1.1:1234"), "203.0.113.10:443", "203.0.113.10"),
92            (
93                Some("[2001:db8:ffff::1]:1234"),
94                "[2001:db8::1]:443",
95                "2001:db8::1",
96            ),
97            (Some("10.0.0.5:1234"), "2001:db8::1", "2001:db8::1"),
98            (Some("198.51.100.2:1234"), "203.0.113.10", "198.51.100.2"),
99            (Some("10.0.0.5:1234"), "not-an-ip", "10.0.0.5"),
100            (Some("10.0.0.5:1234"), "", "10.0.0.5"),
101            (None, "203.0.113.10", "none"),
102        ] {
103            let mut request = HttpRequest::builder()
104                .header("x-forwarded-for", forwarded)
105                .body(Body::empty())
106                .unwrap();
107            if let Some(peer) = peer {
108                request
109                    .extensions_mut()
110                    .insert(ConnectInfo(peer.parse::<SocketAddr>().unwrap()));
111            }
112            let response = router.clone().oneshot(request).await.unwrap();
113            assert_eq!(
114                to_bytes(response.into_body(), 4096).await.unwrap().as_ref(),
115                expected.as_bytes()
116            );
117        }
118    }
119
120    #[test]
121    fn opaque_forwarded_header_and_absent_headers_fall_back_to_peer() {
122        let mut headers = HeaderMap::new();
123        let peer: IpAddr = "10.0.0.5".parse().unwrap();
124        let trusted = vec!["10.0.0.0/8".into()];
125        assert_eq!(real_ip_from_headers(&headers, peer, &trusted), peer);
126        headers.insert("x-forwarded-for", HeaderValue::from_bytes(&[0xff]).unwrap());
127        assert_eq!(real_ip_from_headers(&headers, peer, &trusted), peer);
128    }
129}