Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
f1257d2
packages/ak-arbiter: init
rissson Mar 30, 2026
5294f8a
fixup
rissson Mar 30, 2026
57edeec
add tests
rissson Mar 30, 2026
64b9391
lint
rissson Mar 30, 2026
1e5cb4b
sort out package versions
rissson Mar 30, 2026
e7d3704
packages/ak-config: init
rissson Mar 30, 2026
27ff039
rename to ak-lib
rissson Mar 30, 2026
2bc79f1
Merge branch 'rust-arbiter' into rust-config
rissson Mar 30, 2026
1ee6f11
fixup
rissson Mar 30, 2026
2bab7ed
Merge branch 'rust-arbiter' into rust-config
rissson Mar 30, 2026
d7141df
fixup
rissson Mar 30, 2026
4c54511
move into lib crate
rissson Mar 30, 2026
dc65ab1
packages/ak-lib: init
rissson Mar 30, 2026
524b788
fixup
rissson Mar 30, 2026
a1cf0a7
Merge branch 'rust-lib' into rust-arbiter
rissson Mar 30, 2026
681117d
Merge branch 'rust-arbiter' into rust-config
rissson Mar 30, 2026
48c833c
lint
rissson Mar 30, 2026
341c9cc
Merge branch 'main' into rust-arbiter
rissson Mar 31, 2026
c8fa1b8
Merge branch 'rust-arbiter' into rust-config
rissson Mar 31, 2026
3ef0080
packages/ak-lib/tokio/proxy_procotol: init
rissson Apr 1, 2026
a3fea5d
root: fix rustfmt config
rissson Apr 1, 2026
ab239bf
fix import
rissson Apr 1, 2026
6968d59
Merge branch 'fix-rustfmt-config' into rust-arbiter
rissson Apr 1, 2026
cb1f86b
Merge branch 'fix-rustfmt-config' into rust-config
rissson Apr 1, 2026
75622e9
packages/ak-axum: init
rissson Apr 1, 2026
c08e2a3
wip
rissson Apr 1, 2026
b4f06d6
wip
rissson Apr 1, 2026
0079984
packages/ak-common: rename from ak-lib
rissson Apr 1, 2026
507eb2f
Merge branch 'rust-lib-rename' into rust-arbiter
rissson Apr 1, 2026
7eb07d5
Merge branch 'rust-arbiter' into rust-config
rissson Apr 1, 2026
095d38c
fixup
rissson Apr 1, 2026
365de9d
Merge branch 'rust-lib-rename' into rust-lib-proxy-protocol
rissson Apr 1, 2026
67ed0dc
Merge branch 'rust-config' into rust-axum
rissson Apr 1, 2026
5c937d7
packages/ak-axum/tracing: init
rissson Apr 1, 2026
116e601
packages/ak-axum/server: init
rissson Apr 1, 2026
65f33d6
packages/ak-axum/accept/tls: init
rissson Apr 1, 2026
d999d80
Merge branch 'rust-lib-proxy-protocol' into rust-axum-acceptor-proxy
rissson Apr 1, 2026
2917970
packages/ak-axum/accept/proxy_protocol: init
rissson Apr 1, 2026
279610b
Merge branch 'rust-axum-trace' into rust-axum-extract-trusted-proxy
rissson Apr 1, 2026
89fbf0d
packages/ak-axum/extract/trusted_proxy: init
rissson Apr 1, 2026
e2ab302
add docs
rissson Apr 1, 2026
a30b345
wip
rissson Apr 1, 2026
267336f
fix layer order
rissson Apr 1, 2026
b42fb09
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 1, 2026
5e03610
fixup
rissson Apr 1, 2026
544f662
Merge branch 'rust-axum-trace' into rust-axum-extract-trusted-proxy
rissson Apr 1, 2026
d350e94
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 1, 2026
c2bd441
fixup
rissson Apr 1, 2026
5b13d5b
fixup
rissson Apr 1, 2026
81f314c
Merge branch 'rust-axum-trace' into rust-axum-extract-trusted-proxy
rissson Apr 1, 2026
356c487
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 1, 2026
4c84c53
fixup
rissson Apr 1, 2026
703d5af
fixup
rissson Apr 1, 2026
f74e533
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 1, 2026
da38234
add doc
rissson Apr 1, 2026
d51d860
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 1, 2026
a522b8b
fmt
rissson Apr 1, 2026
a6d543b
Merge branch 'rust-axum-acceptor-proxy' into rust-axum-extract-truste…
rissson Apr 1, 2026
2e5cfb2
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 1, 2026
d2d8b4a
Merge branch 'main' into rust-axum-acceptor-proxy
rissson Apr 8, 2026
75fc6df
fixup
rissson Apr 8, 2026
4b2508f
Merge branch 'rust-axum-acceptor-proxy' into rust-axum-extract-truste…
rissson Apr 8, 2026
f4c7606
Merge branch 'rust-axum-extract-trusted-proxy' into rust-axum-extract…
rissson Apr 8, 2026
e0fbe01
Merge branch 'main' into rust-axum-extract-client-ip
rissson Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
aws-lc-rs = { version = "= 1.16.2", features = ["fips"] }
axum = { version = "= 0.8.8", features = ["http2", "macros", "ws"] }
clap = { version = "= 4.6.0", features = ["derive", "env"] }
client-ip = { version = "0.2.1", features = ["forwarded-header"] }
colored = "= 3.1.1"
config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [
"json",
Expand Down
1 change: 1 addition & 0 deletions packages/ak-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ publish.workspace = true
ak-common.workspace = true
axum-server.workspace = true
axum.workspace = true
client-ip.workspace = true
durstr.workspace = true
eyre.workspace = true
futures.workspace = true
Expand Down
237 changes: 237 additions & 0 deletions packages/ak-axum/src/extract/client_ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//! axum extractor and middleware to retrieve the client IP.

use std::net::{IpAddr, Ipv6Addr, SocketAddr};

use axum::{
Extension, RequestPartsExt as _,
extract::{ConnectInfo, FromRequestParts, Request},
http::request::Parts,
middleware::Next,
response::Response,
};
use tracing::{Span, instrument};

use crate::{accept::proxy_protocol::ProxyProtocolState, extract::trusted_proxy::TrustedProxy};

/// Client IP.
///
/// The [`client_ip_middleware`] must be added to the router before using this extractor,
/// otherwise this will result in requests erroring.
#[derive(Clone, Copy, Debug)]
pub struct ClientIp(pub IpAddr);

impl<S> FromRequestParts<S> for ClientIp
where
S: Send + Sync,
{
type Rejection = <Extension<Self> as FromRequestParts<S>>::Rejection;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Extension::<Self>::from_request_parts(parts, state)
.await
.map(|Extension(client_ip)| client_ip)
}
}

/// Get the client IP from the request.
#[instrument(skip_all)]
async fn extract_client_ip(parts: &mut Parts) -> IpAddr {
let is_trusted = parts
.extract::<TrustedProxy>()
.await
.unwrap_or(TrustedProxy(false))
.0;

if is_trusted {
if let Ok(ip) = client_ip::rightmost_x_forwarded_for(&parts.headers) {
return ip;
}

if let Ok(ip) = client_ip::x_real_ip(&parts.headers) {
return ip;
}

if let Ok(ip) = client_ip::rightmost_forwarded(&parts.headers) {
return ip;
}

if let Ok(Extension(proxy_protocol_state)) =
parts.extract::<Extension<ProxyProtocolState>>().await
&& let Some(header) = &proxy_protocol_state.header
&& let Some(addr) = header.proxied_address()
{
return addr.source.ip();
}
}

if let Ok(ConnectInfo(addr)) = parts.extract::<ConnectInfo<SocketAddr>>().await {
addr.ip()
} else {
// No connect info means we received a request via a Unix socket, hence localhost
// as default.
Ipv6Addr::LOCALHOST.into()
}
}

/// Middleware required by the [`ClientIp`] extractor.
///
/// Use with [`axum::middleware::from_fn`].
pub async fn client_ip_middleware(request: Request, next: Next) -> Response {
let (mut parts, body) = request.into_parts();

let client_ip = extract_client_ip(&mut parts).await;
Span::current().record("remote", client_ip.to_string());
parts.extensions.insert::<ClientIp>(ClientIp(client_ip));

let request = Request::from_parts(parts, body);

next.run(request).await
}

#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;

use axum::{body::Body, http::Request};

use super::*;

#[tokio::test]
async fn x_forwarded_for_trusted() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("x-forwarded-for", "192.0.2.51, 192.0.2.42")
.extension(TrustedProxy(true))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),);
}

#[tokio::test]
async fn x_real_ip_trusted() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("x-real-ip", "192.0.2.42")
.extension(TrustedProxy(true))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),);
}

#[tokio::test]
async fn forwarded_header_trusted() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("forwarded", "for=192.0.2.42")
.extension(TrustedProxy(true))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),);
}

#[tokio::test]
async fn from_connect_info() {
let connect_addr: SocketAddr = "192.0.2.42:34932"
.parse()
.expect("Failed to parse socket address");
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.extension(ConnectInfo(connect_addr))
.extension(TrustedProxy(false))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 42),);
}

#[tokio::test]
async fn headers_untrusted() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("x-forwarded-for", "192.0.2.42")
.extension(TrustedProxy(false))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv6Addr::LOCALHOST);
}

#[tokio::test]
async fn priority_order() {
// Test that X-Forwarded-For takes priority over other headers when trusted
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("x-forwarded-for", "192.0.2.1")
.header("x-real-ip", "192.0.2.2")
.header("forwarded", "for=192.0.2.3")
.extension(TrustedProxy(true))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 1),);
}

#[tokio::test]
async fn no_ip_found() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv6Addr::LOCALHOST);
}

#[tokio::test]
async fn ipv6() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("x-forwarded-for", "2001:db8::42")
.extension(TrustedProxy(true))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x42),);
}

#[tokio::test]
async fn multiple_x_forwarded_for() {
let (mut parts, _) = Request::builder()
.uri("http://example.com/path")
.header("x-forwarded-for", "192.0.2.1, 192.0.2.2, 192.0.2.3")
.extension(TrustedProxy(true))
.body(Body::empty())
.expect("Failed to create request")
.into_parts();

let client_ip = extract_client_ip(&mut parts).await;

assert_eq!(client_ip, Ipv4Addr::new(192, 0, 2, 3),);
}
}
1 change: 1 addition & 0 deletions packages/ak-axum/src/extract/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! axum extractors to get information about a request.

pub mod client_ip;
pub mod trusted_proxy;
5 changes: 3 additions & 2 deletions packages/ak-axum/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tower::ServiceBuilder;
use tower_http::timeout::TimeoutLayer;

use crate::{
extract::trusted_proxy::trusted_proxy_middleware,
extract::{client_ip::client_ip_middleware, trusted_proxy::trusted_proxy_middleware},
tracing::{span_middleware, tracing_middleware},
};

Expand All @@ -27,7 +27,8 @@ pub fn wrap_router(router: Router, with_tracing: bool) -> Router {
timeout,
))
.layer(from_fn(span_middleware))
.layer(from_fn(trusted_proxy_middleware));
.layer(from_fn(trusted_proxy_middleware))
.layer(from_fn(client_ip_middleware));
if with_tracing {
router.layer(service_builder.layer(from_fn(tracing_middleware)))
} else {
Expand Down
3 changes: 2 additions & 1 deletion packages/ak-axum/src/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::collections::HashMap;
use ak_common::config;
use axum::{extract::Request, middleware::Next, response::Response};
use tokio::time::Instant;
use tracing::{Instrument as _, info, info_span, trace};
use tracing::{Instrument as _, field, info, info_span, trace};

/// Create a [`tracing::Span`] for requests.
pub(crate) async fn span_middleware(request: Request, next: Next) -> Response {
Expand All @@ -27,6 +27,7 @@ pub(crate) async fn span_middleware(request: Request, next: Next) -> Response {
"request",
path = %request.uri(),
method = %request.method(),
remote = field::Empty,
http_headers = ?http_headers,
);
next.run(request).instrument(span).await
Expand Down
Loading