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, GatewayError};
13use crate::plugins::{Plugin, PluginExecutionError, 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(
120 &self,
121 ctx: Context,
122 _named_inputs: &HashMap<String, serde_json::Value>,
123 ) -> PluginResult {
124 let addr = &ctx.request.remote_addr;
125
126 if !self.deny.is_empty() && Self::ip_matches(&self.deny, addr) {
128 let mut ctx = ctx;
129 ctx.response.status_code = 403;
130 ctx.response.body =
131 Bytes::from(r#"{"error": "forbidden", "message": "IP address denied"}"#);
132 ctx.response.headers.insert(
133 "content-type".to_string(),
134 vec!["application/json".to_string()],
135 );
136 return Err(PluginExecutionError {
137 context: ctx,
138 error: GatewayError {
139 node_id: String::new(),
140 code: "IP_DENIED".to_string(),
141 message: "IP address denied".to_string(),
142 metadata: HashMap::new(),
143 },
144 });
145 }
146
147 if !self.allow.is_empty() && !Self::ip_matches(&self.allow, addr) {
149 let mut ctx = ctx;
150 ctx.response.status_code = 403;
151 ctx.response.body =
152 Bytes::from(r#"{"error": "forbidden", "message": "IP address not allowed"}"#);
153 ctx.response.headers.insert(
154 "content-type".to_string(),
155 vec!["application/json".to_string()],
156 );
157 return Err(PluginExecutionError {
158 context: ctx,
159 error: GatewayError {
160 node_id: String::new(),
161 code: "IP_NOT_ALLOWED".to_string(),
162 message: "IP address not allowed".to_string(),
163 metadata: HashMap::new(),
164 },
165 });
166 }
167
168 Ok(PluginOutput {
169 context: ctx,
170 named_outputs: HashMap::new(),
171 })
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
183 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
184
185 fn ctx(remote_addr: &str) -> Context {
186 Context {
187 request: GatewayRequest {
188 method: "GET".to_string(),
189 path: "/hello".to_string(),
190 host: "h".to_string(),
191 scheme: "http".to_string(),
192 headers: HashMap::new(),
193 query_params: HashMap::new(),
194 body: Bytes::new(),
195 remote_addr: remote_addr.to_string(),
196 protocol: Protocol::Http1,
197 },
198 response: GatewayResponse {
199 status_code: 0,
200 headers: HashMap::new(),
201 body: Bytes::new(),
202 },
203 message: HashMap::new(),
204 errors: Vec::new(),
205 }
206 }
207
208 fn plugin(config: serde_json::Value) -> IpRestrictionPlugin {
209 let map: HashMap<String, serde_json::Value> =
210 config.as_object().unwrap().clone().into_iter().collect();
211 IpRestrictionPlugin::from_config(&map).unwrap()
212 }
213
214 #[tokio::test]
216 async fn test_allow_cidr_match() {
217 let p = plugin(serde_json::json!({ "allow": ["127.0.0.0/24", "113.74.26.106"] }));
218 assert!(p.execute(ctx("127.0.0.1:5"), &HashMap::new()).await.is_ok());
219 }
220
221 #[tokio::test]
223 async fn test_allow_exact_match() {
224 let p = plugin(serde_json::json!({ "allow": ["127.0.0.0/24", "113.74.26.106"] }));
225 assert!(p
226 .execute(ctx("113.74.26.106:80"), &HashMap::new())
227 .await
228 .is_ok());
229 }
230
231 #[tokio::test]
233 async fn test_allow_unlisted_rejected() {
234 let p = plugin(serde_json::json!({ "allow": ["127.0.0.0/24"] }));
235 let err = p
236 .execute(ctx("114.114.114.114:5"), &HashMap::new())
237 .await
238 .unwrap_err();
239 assert_eq!(err.error.code, "IP_NOT_ALLOWED");
240 assert_eq!(err.context.response.status_code, 403);
241 }
242
243 #[tokio::test]
245 async fn test_deny_match_rejected() {
246 let p = plugin(serde_json::json!({ "deny": ["127.0.0.0/24", "113.74.26.106"] }));
247 let by_cidr = p
248 .execute(ctx("127.0.0.1:5"), &HashMap::new())
249 .await
250 .unwrap_err();
251 assert_eq!(by_cidr.error.code, "IP_DENIED");
252 assert_eq!(by_cidr.context.response.status_code, 403);
253 assert!(p
254 .execute(ctx("113.74.26.106:5"), &HashMap::new())
255 .await
256 .is_err());
257 }
258
259 #[tokio::test]
261 async fn test_deny_unlisted_allowed() {
262 let p = plugin(serde_json::json!({ "deny": ["127.0.0.0/24"] }));
263 assert!(p
264 .execute(ctx("114.114.114.114:5"), &HashMap::new())
265 .await
266 .is_ok());
267 }
268
269 #[tokio::test]
271 async fn test_deny_ipv6() {
272 let p = plugin(serde_json::json!({ "deny": ["::1", "fe80::/32"] }));
273 assert!(p.execute(ctx("::1"), &HashMap::new()).await.is_err()); assert!(p.execute(ctx("fe80::1:1"), &HashMap::new()).await.is_err()); }
276
277 #[tokio::test]
279 async fn test_deny_ipv6_allows_ipv4() {
280 let p = plugin(serde_json::json!({ "deny": ["::1", "fe80::/32"] }));
281 assert!(p.execute(ctx("127.0.0.1:5"), &HashMap::new()).await.is_ok());
282 }
283
284 #[tokio::test]
286 async fn test_deny_precedes_allow() {
287 let p = plugin(serde_json::json!({ "allow": ["10.0.0.1"], "deny": ["10.0.0.1"] }));
288 let err = p
289 .execute(ctx("10.0.0.1:5"), &HashMap::new())
290 .await
291 .unwrap_err();
292 assert_eq!(err.error.code, "IP_DENIED");
293 }
294}