Coder Social home page Coder Social logo

axum-client-ip's Introduction

License Crates.io Docs.rs

axum-client-ip

Client IP address extractors for Axum

Why different extractors?

There are two distinct use cases for client IP which should be treated differently:

  1. You can't tolerate the possibility of spoofing (you're working on rate limiting, spam protection, etc). In this case, you should use [SecureClientIp] or an extractor for a particular header.
  2. You can trade potential spoofing for a statistically better IP determination. E.g. you use the IP for geolocation when the correctness of the location isn't critical for your app. For something like this, you can use [InsecureClientIp].

For a deep dive into the trade-off refer to this Adam Pritchard's article

SecureClientIp vs specific header extractors

Apart from [SecureClientIp] there are [Forwarded], [RightmostForwarded], [XForwardedFor], [RightmostXForwardedFor], [FlyClientIp], [TrueClientIp], [CfConnectingIp] and [XRealIp] extractors.

They work the same way - by extracting IP from the specified header you control. The only difference is in the target header specification. With SecureClientIp you can specify the header at runtime, so you can use e.g. environment variable for this setting (look at the implementation example). While with specific extractors you'd need to recompile your code if you'd like to change the target header (e.g. you're moving to another cloud provider). To mitigate this change you can create a type alias e.g. type InsecureIp = XRealIp and use it in your handlers, then the change will affect only one line.

Usage

use axum::{routing::get, Router};
use axum_client_ip::{InsecureClientIp, SecureClientIp, SecureClientIpSource};
use std::net::SocketAddr;

async fn handler(insecure_ip: InsecureClientIp, secure_ip: SecureClientIp) -> String {
    format!("{insecure_ip:?} {secure_ip:?}")
}

#[tokio::main]
async fn main() {
    async fn handler(insecure_ip: InsecureClientIp, secure_ip: SecureClientIp) -> String {
        format!("{insecure_ip:?} {secure_ip:?}")
    }

    let app = Router::new().route("/", get(handler))
        .layer(SecureClientIpSource::ConnectInfo.into_extension());

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    axum::serve(
        listener,
        // Don't forget to add `ConnectInfo` if you aren't behind a proxy
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .await
    .unwrap()
}

A common issue with Axum extractors

The most often issue with this extractor is using it after one consuming body e.g. [axum::extract::Json]. To fix this rearrange extractors in your handler definition moving body consumption to the end, see details.

Contributing

We appreciate all kinds of contributions, thank you!

Note on README

Most of the readme is automatically copied from the crate documentation by cargo-sync-readme. This way the readme is always in sync with the docs and examples are tested.

So if you find a part of the readme you'd like to change between <!-- cargo-sync-readme start --> and <!-- cargo-sync-readme end --> markers, don't edit README.md directly, but rather change the documentation on top of src/lib.rs and then synchronize the readme with:

cargo sync-readme

(make sure the cargo command is installed):

cargo install cargo-sync-readme

If you have rusty-hook installed the changes will apply automatically on commit.

License

This project is licensed under the MIT license.

axum-client-ip's People

Contributors

64bit avatar adamburgess avatar cole-h avatar colerar avatar dalloriam avatar imbolc avatar jreppnow avatar kavin-kr avatar maxcountryman avatar paolobarbolini avatar predmijat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

axum-client-ip's Issues

SecureClientIp causes 500 when x-forwarded-for is missing

I configured the server to expect x-forwarded-for SecureClientIpSource::RightmostXForwardedFor and the handler with secure_ip: SecureClientIp.

However, when the client does not send the header, the server returns 500. I'd expect 400 or another 4XX error.

Am I missing anything? Is this an expected behaviour?

Make crate more reusable

For example, the SecureClientIp::from_request_parts API is public, but its requirement is too strict.

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {

Caller needs to pass a &mut Parts and a meaningless &() to fill the state argument, and the function is async, so it cannot be called in non-async context.

We can simply modify SecureClientIp::from_parts so it can be used by other crates:

pub fn from(
    ip_source: &SecureClientIpSource,
    headers: &HeaderMap<HeaderValue>,
    extensions: &Extensions,
) -> Result<Self, StringRejection> {
    match ip_source {
        SecureClientIpSource::RightmostForwarded => Forwarded::rightmost_ip(headers),
        SecureClientIpSource::RightmostXForwardedFor => XForwardedFor::rightmost_ip(headers),
        SecureClientIpSource::XRealIp => XRealIp::ip_from_headers(headers),
        SecureClientIpSource::FlyClientIp => FlyClientIp::ip_from_headers(headers),
        SecureClientIpSource::TrueClientIp => TrueClientIp::ip_from_headers(headers),
        SecureClientIpSource::CfConnectingIp => CfConnectingIp::ip_from_headers(headers),
        SecureClientIpSource::ConnectInfo => extensions
            .get::<ConnectInfo<SocketAddr>>()
            .map(|ConnectInfo(addr)| addr.ip())
            .ok_or_else(|| {
                "Can't extract `SecureClientIp`, provide `axum::extract::ConnectInfo`".into()
            }),
    }
    .map(Self)
}

Furthermore, I think we can extract the core function of this crate into another separate crate to avoid copying and pasting:

https://github.com/benwis/tower-governor/blob/df3d175e28c6989ff5e388c8baa990fd440163ba/src/key_extractor.rs#L125-L171

Testing procedure ?

Hi,

I am currently writing a program with your library but while writing test i found that it seems to be no method for forcing a wrong ip to check the behaviour of the program. is there something that i missed ? what are the solutions ?

async fn set_ready(secure_client_ip: SecureClientIp,  state: State<AppState>) -> (StatusCode, String) {
    info!("client ip: {}", secure_client_ip.0);
    if let Some(ip) = state.ip.get() {
        if *ip != secure_client_ip.0 {
            error!("ip mismatch: {} != {}", ip, secure_client_ip.0);
            (StatusCode::FORBIDDEN, "ip mismatch".to_string())
        } else {
            state.is_ready.store(true, Ordering::Relaxed);
            info!("received ready signal from {}", secure_client_ip.0);
            info!("ready !");
            (StatusCode::OK, "ready".to_string())
        }
    } else {
        (StatusCode::BAD_REQUEST, "ip not set yet".to_string())
    }
}

// ... //

#[tokio::test]
    async fn check_set_ready_with_incorrect_ip() {
        let server = setup_test_server();
        server.post("/set_ip").json(&IpAddr { ip: Ipv4Addr::new(127, 0, 0, 1) }).await;
        let res = server.post("/ready")
            .add_header("X-Real-Ip".parse().unwrap(), "12.12.12.12".parse().unwrap())
            .await;
        println!("{:?}", res);
        res.assert_status(StatusCode::FORBIDDEN); // fails here
    }

it seems that the ip source is set to ConnectInfo even if a header is set

(this method works for insecure client tho)

How to get `RightmostXForwardedFor` optional

I'm trying to get the client IP from the RightmostXForwardedFor header. The problem is the app has two configurations. If the app runs on reverse proxy, I want to use the RightmostXForwardedFor otherwise, I want to use the ConnectInfo (which I suppose is the direct client IP).

Routes configuration:

let secure_client_ip_source = if tracker.config.on_reverse_proxy {
    SecureClientIpSource::RightmostXForwardedFor
} else {
    SecureClientIpSource::ConnectInfo
};

Router::new()
    .route("/announce", get(handle).with_state(tracker.clone()))
    .layer(secure_client_ip_source.into_extension())

The handler:

pub async fn handle(
    State(tracker): State<Arc<Tracker>>,
    ExtractAnnounceRequest(announce_request): ExtractAnnounceRequest,
    secure_ip: SecureClientIp,
) -> Response {
// ...
}

But I want something like this:

pub async fn handle(
    State(tracker): State<Arc<Tracker>>,
    ExtractAnnounceRequest(announce_request): ExtractAnnounceRequest,
    maybe_secure_client_ip: Result<SecureClientIp, StringRejection>,
) -> Response {
// ...
}

But that gives me this error:

the trait bound `fn(axum::extract::State<Arc<Tracker>>, ExtractAnnounceRequest, Result<SecureClientIp, StringRejection>) -> impl futures::Future<Output = axum::http::Response<http_body::combinators::box_body::UnsyncBoxBody<axum::body::Bytes, axum::Error>>> {handle}: Handler<_, _, _>` is not satisfied

The reason I want to make it optional is that I want to send a custom error in the handler if the proxy does not provide the X-Forwarded-For header.

Alternatively, I could:

  • Try to catch the extractor error, if that's possible, in Axum.
  • Build a wrapper for your extractor.
  • Just build my extractor and stop using this crate :-(.

For more context, this is the PR I'm working on.

`LeftmostXForwardedFor` for extracting client IP for requests proxied through Cloudflare

Hello,

Just like RightmostXForwardedFor having equivalent LeftmostXForwardedFor will help extract Client IP from X-Forwarded-For header when request gets proxied through Cloudflare. Specially in situations where CF-Connecting-IP header is not available for requests going though other load balancers.

More about why leftmost is in the doc below:

https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#x-forwarded-for

Thank you

Docs are full of 404 errors

Many pages that link to examples lead to https://github.com/imbolc/axum-client-ip/examples/secure.rs which is 404

report error

Hi

Router mode post incoming parameters: Extension,Query,Json

Unable to pass the editor, if the ClientIp library is removed, it will run normally

err:

Handler<_, _, _>` is not satisfied

the trait `Handler<T, S, B2>` is implemented for `Layered<L, H, T, S, B, B2>`

Whitelist reverse proxy IPs?

Other systems that use the X-Forward-For or X-Real-IP headers often allow whitelisting of IPs from which the header is accepted, falling back to the actual TCP peer information instead for requests coming from other sources (or do not include a header of course).

Is there a particular reason this crate does not seem to support this basic security mechanism?

How do I add ConnectInfo

in the docs I see:

// Don't forget to add `ConnetInfo` if you aren't behind a proxy

Since I am running locally I'm not behind a proxy, and I get a message "Can't determine the client IP, check forwarding configuration". How do I add ConnectInfo?

[Question]: How to get client IP address that you can get on the web?

I'm working on rate limiting with user's IP address.

use axum_client_ip::{SecureClientIp, SecureClientIpSource};

async fn handler(secure_ip: SecureClientIp) -> String {
    format!("{}", secure_ip.0)
}

#[tokio::main]
async fn main() {
        let router = Router::new()
            .route("/", get(handler))
            // other routes go here

        axum_server::bind(addr)
            .serve(router.into_make_service_with_connect_info::<SocketAddr>())
            .await
            .unwrap();

With the above code, I get 172.18.0.1 on localhost and 10.0.0.4 on production network with AWS.
These IP addresses are different from what I get on the web by searching What is my IP address.
I would like to get the same IP address using axum-client-ip as the one that I can get by searching my IP address on the web.
How could I achieve this?

get insecure_ip from request

Thanks for such a useful code,but i want to get insecure_ip from request like:

    let remote_addr = req
        .extensions()
        .get::<axum::extract::ConnectInfo<SocketAddr>>()
        .map(|ci| ci.0);

But I don't know how to code,Looking forward to your answer, thank you

An SecureClientIP with a priority fallback system

Hi,

Is it possible to have something like:
CF Header > XRealIP > ConnectInfo

In this case, looking for the CF Header, and if that doesn't exist, try XRealIP, and if that doesn't exist, take the ConnectInfo IP address of client. Someway to add a priority lane, and if none of it works, giving a 500 error eventually to give up.
This would make it easy to host a server behind CloudFlare, but also make it work without CloudFLare.

Thanks !

Axum Middleware Support ?

I need to auth clients based on their IPs on every request so i just thought about using axum middleware but it seems that the client IP is inaccessible from this scope. Am I doing it wrong ? Is it already supported ?

Thanks !

doc: With FlyClientIp and health check enabled - provide header

Hello, This is excellent library everything works as documented.

I have an existing app running on Fly.io with health check enabled.

I integerated axum-client-ip for all paths and FlyClientIp as source for IP. On deployment health check didnt pass - of course because axum-client-ip returns 500 when it did not see the Fly-Client-IP header for health check.

Headers for health check can be configured through services.http_checks.headers

Perhaps it would help to document this somewhere in this repo? if so, I'm happy to send another PR if you think it would help next person and this repo is right place to document it.

Thank you

Add support for getting all IPs in X-Forwarded-For

It seems like SecureClientIpSource only has a RightmostXForwardedFor variant. With GCP LBs, the client IP is the 2nd rightmost IP. Can we add a SecureClientIpSource::XForwardedFor variant?

I see this mentioned in a similar issue but that issue was closed since the author ended up using InsecureClientIp: #24.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.