Skip to main content

rws_server/ip_filter/
mod.rs

1//! IP address allowlist and denylist middleware.
2//!
3//! [`IpFilter`] inspects the client IP from [`crate::server::ConnectionInfo`]
4//! and either allows or blocks the request based on a list of IPv4 or IPv6
5//! addresses and CIDR ranges. An IPv4 rule also matches the same client
6//! connecting over a dual-stack socket as an IPv4-mapped IPv6 address
7//! (`::ffff:1.2.3.4`).
8//!
9//! - **Allow mode** — only listed addresses/ranges pass; all others get `403 Forbidden`.
10//! - **Deny mode** — listed addresses/ranges get `403 Forbidden`; all others pass.
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! # extern crate rws_server as rust_web_server;
16//! use rust_web_server::app::App;
17//! use rust_web_server::core::New;
18//! use rust_web_server::ip_filter::IpFilter;
19//!
20//! // Restrict to internal networks only.
21//! let app = App::new()
22//!     .wrap(IpFilter::allow(["10.0.0.0/8", "192.168.0.0/16", "127.0.0.1"]));
23//!
24//! // Block a known-bad address range.
25//! let app = App::new()
26//!     .wrap(IpFilter::deny(["1.2.3.4", "5.6.7.0/24", "2001:db8::/32"]));
27//! ```
28
29#[cfg(test)]
30mod tests;
31
32use crate::application::Application;
33use crate::middleware::Middleware;
34use crate::request::Request;
35use crate::response::Response;
36use crate::server::ConnectionInfo;
37// Address ranges and `TrustedProxies` live in rws-http (`server_config`
38// holds a `TrustedProxies`); this module adds the middleware.
39pub use rws_http::ip_filter::*;
40
41enum FilterMode {
42    Allow,
43    Deny,
44}
45
46
47
48
49
50
51/// Middleware that filters requests by client IP address.
52///
53/// Constructed via [`IpFilter::allow`] or [`IpFilter::deny`]. Each entry
54/// may be an exact address (`"1.2.3.4"`, `"2001:db8::1"`) or a CIDR range
55/// (`"10.0.0.0/8"`, `"2001:db8::/32"`). Malformed entries are skipped; check
56/// them with [`is_valid_entry`] (`rws --check` does).
57pub struct IpFilter {
58    mode: FilterMode,
59    ranges: Vec<IpRange>,
60}
61
62impl IpFilter {
63    /// Create an allowlist filter. Only IPs matching one of `entries` pass;
64    /// all others receive `403 Forbidden`.
65    ///
66    /// # Example
67    ///
68    /// ```rust,no_run
69    /// # extern crate rws_server as rust_web_server;
70    /// use rust_web_server::ip_filter::IpFilter;
71    ///
72    /// let filter = IpFilter::allow(["127.0.0.1", "10.0.0.0/8"]);
73    /// ```
74    pub fn allow(entries: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
75        Self::from_entries(FilterMode::Allow, entries)
76    }
77
78    /// Create a denylist filter. IPs matching one of `entries` receive
79    /// `403 Forbidden`; all others pass.
80    ///
81    /// # Example
82    ///
83    /// ```rust,no_run
84    /// # extern crate rws_server as rust_web_server;
85    /// use rust_web_server::ip_filter::IpFilter;
86    ///
87    /// let filter = IpFilter::deny(["1.2.3.4", "192.0.2.0/24"]);
88    /// ```
89    pub fn deny(entries: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
90        Self::from_entries(FilterMode::Deny, entries)
91    }
92
93    fn from_entries(mode: FilterMode, entries: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
94        let ranges = entries.into_iter().filter_map(|e| IpRange::parse(e.as_ref())).collect();
95        IpFilter { mode, ranges }
96    }
97
98    fn matches(&self, ip: &str) -> bool {
99        match parse_ip(ip) {
100            Some(addr) => self.ranges.iter().any(|r| (addr & r.mask) == r.network),
101            None => false,
102        }
103    }
104}
105
106/// Whether `ip` is in `entry` (an address or CIDR range); for the WAF's
107/// `cidr` rules.
108#[doc(hidden)]
109pub fn matches(entry: &str, ip: &str) -> bool {
110    IpFilter::deny([entry]).matches(ip)
111}
112
113impl Middleware for IpFilter {
114    /// Headers only: a streamed body passes through untouched.
115    fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
116        true
117    }
118
119
120    fn handle(
121        &self,
122        request: &Request,
123        connection: &ConnectionInfo,
124        next: &dyn Application,
125    ) -> Result<Response, String> {
126        use crate::error::{AppError, IntoResponse};
127        let matched = self.matches(&client_ip(request, &connection.client.ip));
128        match self.mode {
129            FilterMode::Allow if !matched => Ok(AppError::Forbidden.into_response()),
130            FilterMode::Deny if matched => Ok(AppError::Forbidden.into_response()),
131            _ => next.execute(request, connection),
132        }
133    }
134
135    /// In-memory only: safe on the event loop.
136    fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
137        true
138    }
139}
140
141