Skip to content

Commit 76997e0

Browse files
authored
Support for X-Envoy-External-Address (#41)
1 parent 2132c37 commit 76997e0

3 files changed

Lines changed: 47 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ serde = ["dep:serde"]
2121

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

2727
[dev-dependencies]

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ application independent from a proxy it can run behind (if any) and also
2424
separate extractors for each proxy / source header.
2525

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

src/lib.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ define_extractor!(
8787
client_ip::true_client_ip
8888
);
8989

90+
define_extractor!(
91+
/// Extracts an IP from `X-Envoy-External-Address` (Envoy, Istio) header
92+
XEnvoyExternalAddress,
93+
client_ip::x_envoy_external_address
94+
);
95+
9096
define_extractor!(
9197
/// Extracts an IP from `X-Real-Ip` (Nginx) header
9298
XRealIp,
@@ -126,6 +132,8 @@ pub enum ClientIpSource {
126132
RightmostXForwardedFor,
127133
/// IP from the `True-Client-IP` header
128134
TrueClientIp,
135+
/// IP from the `X-Envoy-External-Address` address
136+
XEnvoyExternalAddress,
129137
/// IP from the `X-Real-Ip` header
130138
XRealIp,
131139
}
@@ -164,6 +172,7 @@ impl FromStr for ClientIpSource {
164172
"RightmostForwarded" => Self::RightmostForwarded,
165173
"RightmostXForwardedFor" => Self::RightmostXForwardedFor,
166174
"TrueClientIp" => Self::TrueClientIp,
175+
"XEnvoyExternalAddress" => Self::XEnvoyExternalAddress,
167176
"XRealIp" => Self::XRealIp,
168177
_ => return Err(ParseClientIpSourceError(s.to_string())),
169178
})
@@ -183,6 +192,7 @@ impl fmt::Display for ClientIpSource {
183192
ClientIpSource::RightmostForwarded => "RightmostForwarded",
184193
ClientIpSource::RightmostXForwardedFor => "RightmostXForwardedFor",
185194
ClientIpSource::TrueClientIp => "TrueClientIp",
195+
ClientIpSource::XEnvoyExternalAddress => "XEnvoyExternalAddress",
186196
ClientIpSource::XRealIp => "XRealIp",
187197
})
188198
}
@@ -219,6 +229,9 @@ where
219229
RightmostXForwardedFor::ip_from_headers(&parts.headers)
220230
}
221231
ClientIpSource::TrueClientIp => TrueClientIp::ip_from_headers(&parts.headers),
232+
ClientIpSource::XEnvoyExternalAddress => {
233+
XEnvoyExternalAddress::ip_from_headers(&parts.headers)
234+
}
222235
ClientIpSource::XRealIp => XRealIp::ip_from_headers(&parts.headers),
223236
}
224237
.map(Self)
@@ -289,9 +302,7 @@ mod tests {
289302

290303
#[cfg(feature = "forwarded-header")]
291304
use super::RightmostForwarded;
292-
use super::{
293-
CfConnectingIp, ClientIpSource, FlyClientIp, RightmostXForwardedFor, TrueClientIp, XRealIp,
294-
};
305+
use super::{CfConnectingIp, ClientIpSource, FlyClientIp, RightmostXForwardedFor, TrueClientIp, XEnvoyExternalAddress, XRealIp};
295306
use crate::CloudFrontViewerAddress;
296307

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

503+
#[tokio::test]
504+
async fn x_envoy_external_address() {
505+
let header = "x-envoy-external-address";
506+
507+
fn app() -> Router {
508+
Router::new().route("/", get(|ip: XEnvoyExternalAddress| async move { ip.0.to_string() }))
509+
}
510+
511+
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
512+
let resp = app().oneshot(req).await.unwrap();
513+
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
514+
515+
let req = Request::builder()
516+
.uri("/")
517+
.header(header, VALID_IPV4)
518+
.body(Body::empty())
519+
.unwrap();
520+
let resp = app().oneshot(req).await.unwrap();
521+
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
522+
523+
let req = Request::builder()
524+
.uri("/")
525+
.header(header, VALID_IPV6)
526+
.body(Body::empty())
527+
.unwrap();
528+
let resp = app().oneshot(req).await.unwrap();
529+
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
530+
}
531+
492532
#[tokio::test]
493533
async fn x_real_ip() {
494534
let header = "x-real-ip";
@@ -539,6 +579,7 @@ mod tests {
539579
assert_match(ClientIpSource::RightmostForwarded);
540580
assert_match(ClientIpSource::RightmostXForwardedFor);
541581
assert_match(ClientIpSource::TrueClientIp);
582+
assert_match(ClientIpSource::XEnvoyExternalAddress);
542583
assert_match(ClientIpSource::XRealIp);
543584
}
544585
}

0 commit comments

Comments
 (0)