featherbit/plugins/native/
ip_restriction.rs1use async_trait::async_trait;
8use bytes::Bytes;
9use std::collections::HashMap;
10use std::net::IpAddr;
11
12use crate::context::Context;
13use crate::plugins::{Plugin, PluginOutput, PluginResult};
14
15pub struct IpRestrictionPlugin {
23 allow: Vec<String>,
25 deny: Vec<String>,
27}
28
29impl IpRestrictionPlugin {
30 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
46 let allow = config
47 .get("allow")
48 .and_then(|v| v.as_array())
49 .map(|seq| {
50 seq.iter()
51 .filter_map(|v| v.as_str().map(String::from))
52 .collect()
53 })
54 .unwrap_or_default();
55
56 let deny = config
57 .get("deny")
58 .and_then(|v| v.as_array())
59 .map(|seq| {
60 seq.iter()
61 .filter_map(|v| v.as_str().map(String::from))
62 .collect()
63 })
64 .unwrap_or_default();
65
66 Ok(Self { allow, deny })
67 }
68
69 fn ip_matches(patterns: &[String], addr: &str) -> bool {
73 let ip: IpAddr = match addr.parse() {
74 Ok(ip) => ip,
75 Err(_) => {
76 match addr.rsplit_once(':').and_then(|(ip, _)| ip.parse().ok()) {
78 Some(ip) => ip,
79 None => return false,
80 }
81 }
82 };
83
84 for pattern in patterns {
85 if pattern == addr || pattern == &ip.to_string() {
86 return true;
87 }
88 if let Some((net, bits)) = pattern.split_once('/') {
90 if let (Ok(net_ip), Ok(bits)) = (net.parse::<IpAddr>(), bits.parse::<u32>()) {
91 match (net_ip, ip) {
92 (IpAddr::V4(net), IpAddr::V4(addr)) => {
93 let mask = if bits == 0 { 0 } else { !0u32 << (32 - bits) };
94 if u32::from(net) & mask == u32::from(addr) & mask {
95 return true;
96 }
97 }
98 (IpAddr::V6(net), IpAddr::V6(addr)) => {
99 let mask = if bits == 0 { 0 } else { !0u128 << (128 - bits) };
100 if u128::from(net) & mask == u128::from(addr) & mask {
101 return true;
102 }
103 }
104 _ => {}
105 }
106 }
107 }
108 }
109 false
110 }
111}
112
113#[async_trait]
114impl Plugin for IpRestrictionPlugin {
115 fn plugin_type(&self) -> &str {
116 "ip-restriction"
117 }
118
119 async fn execute(&self, ctx: Context) -> PluginResult {
120 let addr = &ctx.request.remote_addr;
121
122 if !self.deny.is_empty() && Self::ip_matches(&self.deny, addr) {
124 let mut ctx = ctx;
125 ctx.response.status_code = 403;
126 ctx.response.body =
127 Bytes::from(r#"{"error": "forbidden", "message": "IP address denied"}"#);
128 ctx.response.headers.insert(
129 "content-type".to_string(),
130 vec!["application/json".to_string()],
131 );
132 return Ok(PluginOutput::on_port(ctx, "denied"));
133 }
134
135 if !self.allow.is_empty() && !Self::ip_matches(&self.allow, addr) {
137 let mut ctx = ctx;
138 ctx.response.status_code = 403;
139 ctx.response.body =
140 Bytes::from(r#"{"error": "forbidden", "message": "IP address not allowed"}"#);
141 ctx.response.headers.insert(
142 "content-type".to_string(),
143 vec!["application/json".to_string()],
144 );
145 return Ok(PluginOutput::on_port(ctx, "denied"));
146 }
147
148 Ok(PluginOutput::success(ctx))
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
160 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
161
162 fn ctx(remote_addr: &str) -> Context {
163 Context {
164 request: GatewayRequest {
165 method: "GET".to_string(),
166 path: "/hello".to_string(),
167 host: "h".to_string(),
168 scheme: "http".to_string(),
169 headers: HashMap::new(),
170 query_params: HashMap::new(),
171 body: Bytes::new(),
172 remote_addr: remote_addr.to_string(),
173 protocol: Protocol::Http1,
174 },
175 response: GatewayResponse {
176 status_code: 0,
177 headers: HashMap::new(),
178 body: Bytes::new(),
179 stream: None,
180 },
181 message: HashMap::new(),
182 errors: Vec::new(),
183 }
184 }
185
186 fn plugin(config: serde_json::Value) -> IpRestrictionPlugin {
187 let map: HashMap<String, serde_json::Value> =
188 config.as_object().unwrap().clone().into_iter().collect();
189 IpRestrictionPlugin::from_config(&map).unwrap()
190 }
191
192 #[tokio::test]
194 async fn test_allow_cidr_match() {
195 let p = plugin(serde_json::json!({ "allow": ["127.0.0.0/24", "113.74.26.106"] }));
196 assert!(p.execute(ctx("127.0.0.1:5")).await.unwrap().port.is_none());
197 }
198
199 #[tokio::test]
201 async fn test_allow_exact_match() {
202 let p = plugin(serde_json::json!({ "allow": ["127.0.0.0/24", "113.74.26.106"] }));
203 assert!(p
204 .execute(ctx("113.74.26.106:80"))
205 .await
206 .unwrap()
207 .port
208 .is_none());
209 }
210
211 #[tokio::test]
213 async fn test_allow_unlisted_rejected() {
214 let p = plugin(serde_json::json!({ "allow": ["127.0.0.0/24"] }));
215 let out = p.execute(ctx("114.114.114.114:5")).await.unwrap();
216 assert_eq!(out.port, Some("denied"));
217 assert_eq!(out.context.response.status_code, 403);
218 }
219
220 #[tokio::test]
222 async fn test_deny_match_rejected() {
223 let p = plugin(serde_json::json!({ "deny": ["127.0.0.0/24", "113.74.26.106"] }));
224 let by_cidr = p.execute(ctx("127.0.0.1:5")).await.unwrap();
225 assert_eq!(by_cidr.port, Some("denied"));
226 assert_eq!(by_cidr.context.response.status_code, 403);
227 let out = p.execute(ctx("113.74.26.106:5")).await.unwrap();
228 assert_eq!(out.port, Some("denied"));
229 }
230
231 #[tokio::test]
233 async fn test_deny_unlisted_allowed() {
234 let p = plugin(serde_json::json!({ "deny": ["127.0.0.0/24"] }));
235 assert!(p
236 .execute(ctx("114.114.114.114:5"))
237 .await
238 .unwrap()
239 .port
240 .is_none());
241 }
242
243 #[tokio::test]
245 async fn test_deny_ipv6() {
246 let p = plugin(serde_json::json!({ "deny": ["::1", "fe80::/32"] }));
247 assert_eq!(p.execute(ctx("::1")).await.unwrap().port, Some("denied")); assert_eq!(
249 p.execute(ctx("fe80::1:1")).await.unwrap().port,
250 Some("denied")
251 ); }
253
254 #[tokio::test]
256 async fn test_deny_ipv6_allows_ipv4() {
257 let p = plugin(serde_json::json!({ "deny": ["::1", "fe80::/32"] }));
258 assert!(p.execute(ctx("127.0.0.1:5")).await.unwrap().port.is_none());
259 }
260
261 #[tokio::test]
263 async fn test_deny_precedes_allow() {
264 let p = plugin(serde_json::json!({ "allow": ["10.0.0.1"], "deny": ["10.0.0.1"] }));
265 let out = p.execute(ctx("10.0.0.1:5")).await.unwrap();
266 assert_eq!(out.port, Some("denied"));
267 }
268}