IP Filtering

Allow or deny requests by client IPv4 or IPv6 address or CIDR range with the IpFilter middleware.

Quick start

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::ip_filter::IpFilter;

// Allowlist — only internal networks pass
let app = App::new()
    .wrap(IpFilter::allow(["10.0.0.0/8", "192.168.0.0/16", "127.0.0.1"]));

// Denylist — block a known-bad range
let app = App::new()
    .wrap(IpFilter::deny(["1.2.3.4", "5.6.7.0/24"]));

Modes

IpFilter::allow(entries)

Allowlist mode. Only requests from IPs that match one of the entries pass through. All others receive 403 Forbidden.

IpFilter::allow([
    "127.0.0.1",          // exact loopback
    "10.0.0.0/8",         // entire 10.x.x.x range
    "192.168.1.0/24",     // specific subnet
])

IpFilter::deny(entries)

Denylist mode. Requests from IPs that match one of the entries receive 403 Forbidden. All others pass through.

IpFilter::deny([
    "1.2.3.4",            // single bad actor
    "192.0.2.0/24",       // TEST-NET-1 documentation range
])

Combining both

Stack two filters to allow a network but carve out one address inside it — a request must pass every layer:

let app = App::new()
    .wrap(IpFilter::allow(["10.0.0.0/8"]))
    .wrap(IpFilter::deny(["10.0.0.99"]));

Entry formats

Each entry in the list is an exact IPv4 or IPv6 address, or a CIDR range:

Format Example Matches
Exact address "10.0.0.1" Only 10.0.0.1
CIDR /8 "10.0.0.0/8" 10.0.0.0 – 10.255.255.255
CIDR /16 "192.168.0.0/16" 192.168.0.0 – 192.168.255.255
CIDR /24 "192.168.1.0/24" 192.168.1.0 – 192.168.1.255
CIDR /32 "10.0.0.1/32" Only 10.0.0.1
IPv6 address "2001:db8::1" Only 2001:db8::1
IPv6 CIDR "2001:db8::/32" 2001:db8:: – 2001:db8:ffff:…

Malformed entries (invalid syntax, a prefix longer than 32 for IPv4 or 128 for IPv6) are skipped at construction time. ip_filter::is_valid_entry(entry) tells you whether an entry parses, and rws --check reports invalid entries in rws.config.toml.

IPv4 and IPv6 clients

rws listens on IPv4 and IPv6. An IPv4 client reaching a dual-stack socket is reported as an IPv4-mapped address (::ffff:10.1.2.3); IPv4 rules match it exactly as they match 10.1.2.3. IPv4 rules never match native IPv6 clients and vice versa, so a deny list meant for everyone needs both families — for example ["0.0.0.0/0", "::/0"].

Use case: restrict admin routes to internal network

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::ip_filter::IpFilter;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use std::sync::Arc;

struct State;

let admin_only = IpFilter::allow(["10.0.0.0/8", "127.0.0.1"]);

let app = App::with_state(Arc::new(State))
    .get("/admin/metrics", |_req, _params, _conn, _state| {
        // only reachable from internal IPs
        Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "metrics data")
    })
    .wrap(admin_only);

For more granular control — applying the filter only to /admin/* routes rather than the whole app — wrap a separate app that holds only those routes instead of the top-level app:

use rust_web_server::app::App;
use rust_web_server::ip_filter::IpFilter;

// An app with only the admin routes, wrapped with IP filtering
let protected = App::with_state(())
    .get("/admin/config", admin_config_handler)
    .get("/admin/metrics", admin_metrics_handler)
    .wrap(IpFilter::allow(["10.0.0.0/8", "127.0.0.1"]));

Composing with other middleware

IpFilter implements Middleware and stacks cleanly with rate limiting, authentication, and rewrite layers:

use rust_web_server::middleware::RateLimitLayer;
use rust_web_server::auth::BasicAuthLayer;

let app = App::new()
    .wrap(IpFilter::allow(["10.0.0.0/8"]))  // checked first
    .wrap(BasicAuthLayer::new(|u, p| u == "user" && p == "pass"))
    .wrap(RateLimitLayer);
Client IP source

The client IP is read from ConnectionInfo::client.ip, which is the remote address of the TCP connection. If the server sits behind a load balancer or reverse proxy, configure the proxy to forward the real client IP and adjust your handler logic accordingly — IpFilter does not inspect X-Forwarded-For headers.

Edit this page on GitHub