feat: Introduce accepted_ip_sources as a multiple options variant of request_ip_source

The current `request_ip_source` setting only allows a single option.
While it does fall back to the peer IP if the header is missing as of !2003,
which likely covers a lot of regular use, only allowing a single option limits
the deployment options available to more advanced deployments.
Setups where internal and external traffic use different reverse proxies will
end up with the wrong IP and the implicitness of the fallback allows for
situations where the used IP is not the IP expected.

By introducing a setting that allows multiple options to be set,
this limitation is resolved and it becomes possible to have client IP resolution
behind different reverse proxies and also making it possible to decide if and/or
what the fallback should be.

If set, options are evaluated in order. If all fail, the request fails.
This commit is contained in:
Omar Pakker authored and Ellis Git committed 2026-07-21 14:35:55 +00:00
1 parent 480e975877
commit b9c6d5956f
9 files changed
+244 -132

No files matched your search

-1
View File
@@ -91,7 +91,6 @@ zstd_compression = [
]
[dependencies]
axum-client-ip.workspace = true
axum-server-dual-protocol.workspace = true
axum-server-dual-protocol.optional = true
axum-server.workspace = true
+6 -22
View File
@@ -1,11 +1,11 @@
use std::{any::Any, sync::Arc, time::Duration};
use axum::{
Router, extract,
extract::{DefaultBodyLimit, FromRequestParts, MatchedPath},
Router,
extract::{self, DefaultBodyLimit, FromRequestParts, MatchedPath, State},
};
use axum_client_ip::{ClientIp, ClientIpSource};
use conduwuit::{Result, Server, debug, error};
use conduwuit_api::client_ip::ClientIp;
use conduwuit_service::{Services, state::Guard};
use http::{
HeaderValue, Method, StatusCode,
@@ -47,26 +47,9 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
))]
let layers = layers.layer(compression_layer(server));
let client_ip_layer = match services
.config
.request_ip_source
.as_ref()
.map(AsRef::as_ref)
{
| Some("cf_connecting_ip") => ClientIpSource::CfConnectingIp,
| Some("cloudfront_viewer_address") => ClientIpSource::CloudFrontViewerAddress,
| Some("fly_client_ip") => ClientIpSource::FlyClientIp,
| Some("x_forwarded_for") => ClientIpSource::RightmostXForwardedFor,
| Some("true_client_ip") => ClientIpSource::TrueClientIp,
| Some("x_envoy_external_address") => ClientIpSource::XEnvoyExternalAddress,
| Some("x_real_ip") => ClientIpSource::XRealIp,
| None | Some(_) => ClientIpSource::ConnectInfo,
};
let services_ = services.clone();
let layers = layers
.layer(SetSensitiveHeadersLayer::new([header::AUTHORIZATION]))
.layer(client_ip_layer.into_extension())
.layer(
TraceLayer::new_for_http()
.make_span_with(tracing_span::<_>)
@@ -74,7 +57,7 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
.on_request(DefaultOnRequest::new().level(tracing::Level::TRACE))
.on_response(DefaultOnResponse::new().level(tracing::Level::DEBUG)),
)
.layer(axum::middleware::from_fn(request_ip))
.layer(axum::middleware::from_fn_with_state(Arc::clone(services), request_ip))
.layer(axum::middleware::from_fn_with_state(Arc::clone(services), request::handle))
.layer(ResponseBodyTimeoutLayer::new(Duration::from_secs(
server.config.client_response_timeout,
@@ -238,11 +221,12 @@ fn tracing_span<T>(request: &http::Request<T>) -> tracing::Span {
/// Annotates the tracing span with the client IP
async fn request_ip(
State(services): State<Arc<Services>>,
request: extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let (mut parts, body) = request.into_parts();
if let Ok(ip) = ClientIp::from_request_parts(&mut parts, &()).await {
if let Ok(ip) = ClientIp::from_request_parts(&mut parts, &services).await {
let span = tracing::Span::current();
span.record("ip", ip.0.to_string());
}