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