aster_forge_middleware/axum/
client_ip.rs1use axum::extract::{ConnectInfo, Request};
3use http::HeaderMap;
4use ipnet::IpNet;
5use std::net::{IpAddr, SocketAddr};
6
7#[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#[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#[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#[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}