Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ serde = ["dep:serde"]

[dependencies]
axum = { version = "0.8", default-features = false }
client-ip = "0.1"
client-ip = "0.2"
serde = { version = "1", features = ["derive"], optional = true }

[dev-dependencies]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ application independent from a proxy it can run behind (if any) and also
separate extractors for each proxy / source header.

| Extractor / `ClientIpSource` Variant | Header Used | Typical Proxy / Service |
| ------------------------------------ | --------------------------- | ------------------------------------------------------- |
|--------------------------------------| --------------------------- |---------------------------------------------------------|
| [`CfConnectingIp`] | `CF-Connecting-IP` | Cloudflare |
| [`CloudFrontViewerAddress`] | `CloudFront-Viewer-Address` | AWS CloudFront |
| [`FlyClientIp`] | `Fly-Client-IP` | Fly.io |
| [`RightmostForwarded`] | `Forwarded` | Proxies supporting RFC 7239 (extracts rightmost `for=`) |
| [`RightmostXForwardedFor`] | `X-Forwarded-For` | Nginx, Apache, HAProxy, CDNs, LBs |
| [`TrueClientIp`] | `True-Client-IP` | Cloudflare, Akamai |
| [`XEnvoyExternalAddress`] | `X-Envoy-External-Address` | Envoy, Istio |
| [`XRealIp`] | `X-Real-Ip` | Nginx |
| [`ConnectInfo`] | N/A (uses socket address) | No proxy, e.g. listening directly to 80 port |

Expand Down
47 changes: 44 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ define_extractor!(
client_ip::true_client_ip
);

define_extractor!(
/// Extracts an IP from `X-Envoy-External-Address` (Envoy, Istio) header
XEnvoyExternalAddress,
client_ip::x_envoy_external_address
);

define_extractor!(
/// Extracts an IP from `X-Real-Ip` (Nginx) header
XRealIp,
Expand Down Expand Up @@ -126,6 +132,8 @@ pub enum ClientIpSource {
RightmostXForwardedFor,
/// IP from the `True-Client-IP` header
TrueClientIp,
/// IP from the `X-Envoy-External-Address` address
XEnvoyExternalAddress,
/// IP from the `X-Real-Ip` header
XRealIp,
}
Expand Down Expand Up @@ -164,6 +172,7 @@ impl FromStr for ClientIpSource {
"RightmostForwarded" => Self::RightmostForwarded,
"RightmostXForwardedFor" => Self::RightmostXForwardedFor,
"TrueClientIp" => Self::TrueClientIp,
"XEnvoyExternalAddress" => Self::XEnvoyExternalAddress,
"XRealIp" => Self::XRealIp,
_ => return Err(ParseClientIpSourceError(s.to_string())),
})
Expand All @@ -183,6 +192,7 @@ impl fmt::Display for ClientIpSource {
ClientIpSource::RightmostForwarded => "RightmostForwarded",
ClientIpSource::RightmostXForwardedFor => "RightmostXForwardedFor",
ClientIpSource::TrueClientIp => "TrueClientIp",
ClientIpSource::XEnvoyExternalAddress => "XEnvoyExternalAddress",
ClientIpSource::XRealIp => "XRealIp",
})
}
Expand Down Expand Up @@ -219,6 +229,9 @@ where
RightmostXForwardedFor::ip_from_headers(&parts.headers)
}
ClientIpSource::TrueClientIp => TrueClientIp::ip_from_headers(&parts.headers),
ClientIpSource::XEnvoyExternalAddress => {
XEnvoyExternalAddress::ip_from_headers(&parts.headers)
}
ClientIpSource::XRealIp => XRealIp::ip_from_headers(&parts.headers),
}
.map(Self)
Expand Down Expand Up @@ -289,9 +302,7 @@ mod tests {

#[cfg(feature = "forwarded-header")]
use super::RightmostForwarded;
use super::{
CfConnectingIp, ClientIpSource, FlyClientIp, RightmostXForwardedFor, TrueClientIp, XRealIp,
};
use super::{CfConnectingIp, ClientIpSource, FlyClientIp, RightmostXForwardedFor, TrueClientIp, XEnvoyExternalAddress, XRealIp};
use crate::CloudFrontViewerAddress;

const VALID_IPV4: &str = "1.2.3.4";
Expand Down Expand Up @@ -489,6 +500,35 @@ mod tests {
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}

#[tokio::test]
async fn x_envoy_external_address() {
let header = "x-envoy-external-address";

fn app() -> Router {
Router::new().route("/", get(|ip: XEnvoyExternalAddress| async move { ip.0.to_string() }))
}

let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);

let req = Request::builder()
.uri("/")
.header(header, VALID_IPV4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);

let req = Request::builder()
.uri("/")
.header(header, VALID_IPV6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}

#[tokio::test]
async fn x_real_ip() {
let header = "x-real-ip";
Expand Down Expand Up @@ -539,6 +579,7 @@ mod tests {
assert_match(ClientIpSource::RightmostForwarded);
assert_match(ClientIpSource::RightmostXForwardedFor);
assert_match(ClientIpSource::TrueClientIp);
assert_match(ClientIpSource::XEnvoyExternalAddress);
assert_match(ClientIpSource::XRealIp);
}
}
Loading