huginn_net_http/filter.rs
1use pnet::ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4/// Filter mode: Allow (allowlist) or Deny (denylist)
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub enum FilterMode {
7 /// Allow only matching packets (allowlist mode)
8 #[default]
9 Allow,
10 /// Deny matching packets (denylist mode)
11 Deny,
12}
13
14/// Port filter configuration
15///
16/// Filters packets based on TCP source and/or destination ports.
17/// Supports individual ports, ranges, and lists.
18///
19/// # Examples
20///
21/// ```rust
22/// use huginn_net_http::PortFilter;
23///
24/// // Single port
25/// let filter = PortFilter::new().destination(443);
26///
27/// // Multiple ports
28/// let filter = PortFilter::new().destination_list(vec![80, 443, 8080]);
29///
30/// // Port range
31/// let filter = PortFilter::new().destination_range(8000..9000);
32/// ```
33#[derive(Debug, Clone, Default)]
34pub struct PortFilter {
35 /// Source ports to match
36 pub source_ports: Vec<u16>,
37 /// Destination ports to match
38 pub destination_ports: Vec<u16>,
39 /// Source port ranges (inclusive)
40 pub source_ranges: Vec<(u16, u16)>,
41 /// Destination port ranges (inclusive)
42 pub destination_ranges: Vec<(u16, u16)>,
43 /// Match ANY port (source OR destination)?
44 pub match_any: bool,
45}
46
47impl PortFilter {
48 /// Create a new empty port filter
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 /// Add a destination port
54 ///
55 /// # Examples
56 ///
57 /// ```rust
58 /// use huginn_net_http::PortFilter;
59 ///
60 /// let filter = PortFilter::new().destination(443);
61 /// ```
62 pub fn destination(mut self, port: u16) -> Self {
63 self.destination_ports.push(port);
64 self
65 }
66
67 /// Add a source port
68 ///
69 /// # Examples
70 ///
71 /// ```rust
72 /// use huginn_net_http::PortFilter;
73 ///
74 /// let filter = PortFilter::new().source(12345);
75 /// ```
76 pub fn source(mut self, port: u16) -> Self {
77 self.source_ports.push(port);
78 self
79 }
80
81 /// Add a destination port range (inclusive)
82 ///
83 /// # Examples
84 ///
85 /// ```rust
86 /// use huginn_net_http::PortFilter;
87 ///
88 /// let filter = PortFilter::new().destination_range(8000..9000);
89 /// // Matches ports 8000 through 8999
90 /// ```
91 pub fn destination_range(mut self, range: std::ops::Range<u16>) -> Self {
92 self.destination_ranges
93 .push((range.start, range.end.saturating_sub(1)));
94 self
95 }
96
97 /// Add a source port range (inclusive)
98 ///
99 /// # Examples
100 ///
101 /// ```rust
102 /// use huginn_net_http::PortFilter;
103 ///
104 /// let filter = PortFilter::new().source_range(10000..20000);
105 /// // Matches ports 10000 through 19999
106 /// ```
107 pub fn source_range(mut self, range: std::ops::Range<u16>) -> Self {
108 self.source_ranges
109 .push((range.start, range.end.saturating_sub(1)));
110 self
111 }
112
113 /// Add multiple destination ports
114 ///
115 /// # Examples
116 ///
117 /// ```rust
118 /// use huginn_net_http::PortFilter;
119 ///
120 /// let filter = PortFilter::new().destination_list(vec![80, 443, 8080, 8443]);
121 /// ```
122 pub fn destination_list(mut self, ports: Vec<u16>) -> Self {
123 self.destination_ports.extend(ports);
124 self
125 }
126
127 /// Add multiple source ports
128 ///
129 /// # Examples
130 ///
131 /// ```rust
132 /// use huginn_net_http::PortFilter;
133 ///
134 /// let filter = PortFilter::new().source_list(vec![12345, 54321, 9999]);
135 /// ```
136 pub fn source_list(mut self, ports: Vec<u16>) -> Self {
137 self.source_ports.extend(ports);
138 self
139 }
140
141 /// Match if ANY port matches (source OR destination)
142 ///
143 /// By default, all specified filters must match. With `match_any()`,
144 /// the packet passes if either source OR destination matches.
145 pub fn any_port(mut self) -> Self {
146 self.match_any = true;
147 self
148 }
149
150 /// Check if packet matches port filter
151 ///
152 /// # Returns
153 ///
154 /// `true` if the packet matches the filter criteria
155 pub fn matches(&self, src_port: u16, dst_port: u16) -> bool {
156 if self.match_any {
157 let all_ports: Vec<u16> = self
158 .source_ports
159 .iter()
160 .chain(self.destination_ports.iter())
161 .copied()
162 .collect();
163
164 let all_ranges: Vec<(u16, u16)> = self
165 .source_ranges
166 .iter()
167 .chain(self.destination_ranges.iter())
168 .copied()
169 .collect();
170
171 let port_match = all_ports.contains(&src_port)
172 || all_ports.contains(&dst_port)
173 || all_ranges
174 .iter()
175 .any(|(start, end)| src_port >= *start && src_port <= *end)
176 || all_ranges
177 .iter()
178 .any(|(start, end)| dst_port >= *start && dst_port <= *end);
179
180 port_match
181 } else {
182 let src_match = self.source_ports.contains(&src_port)
183 || self
184 .source_ranges
185 .iter()
186 .any(|(start, end)| src_port >= *start && src_port <= *end);
187
188 let dst_match = self.destination_ports.contains(&dst_port)
189 || self
190 .destination_ranges
191 .iter()
192 .any(|(start, end)| dst_port >= *start && dst_port <= *end);
193
194 let src_ok = self.source_ports.is_empty() && self.source_ranges.is_empty() || src_match;
195 let dst_ok = self.destination_ports.is_empty() && self.destination_ranges.is_empty()
196 || dst_match;
197 src_ok && dst_ok
198 }
199 }
200}
201
202/// IP address filter configuration
203///
204/// Filters packets based on specific IPv4 or IPv6 addresses.
205///
206/// # Examples
207///
208/// ```rust
209/// use huginn_net_http::IpFilter;
210///
211/// let filter = IpFilter::new()
212/// .allow("8.8.8.8")
213/// .unwrap()
214/// .allow("2001:4860:4860::8888")
215/// .unwrap();
216/// ```
217#[derive(Debug, Clone, Default)]
218pub struct IpFilter {
219 /// IPv4 addresses to match
220 pub ipv4_addresses: Vec<Ipv4Addr>,
221 /// IPv6 addresses to match
222 pub ipv6_addresses: Vec<Ipv6Addr>,
223 /// Check source, destination, or both?
224 pub check_source: bool,
225 pub check_destination: bool,
226}
227
228impl IpFilter {
229 /// Create a new IP filter that checks both source and destination by default
230 pub fn new() -> Self {
231 Self { check_source: true, check_destination: true, ..Default::default() }
232 }
233
234 /// Add an IP address (auto-detects IPv4/IPv6)
235 ///
236 /// # Errors
237 ///
238 /// Returns an error if the IP address string is invalid
239 ///
240 /// # Examples
241 ///
242 /// ```rust
243 /// use huginn_net_http::IpFilter;
244 ///
245 /// let filter = IpFilter::new()
246 /// .allow("192.168.1.1").unwrap()
247 /// .allow("2001:db8::1").unwrap();
248 /// ```
249 pub fn allow(mut self, ip: &str) -> Result<Self, String> {
250 let addr: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {e}"))?;
251 match addr {
252 IpAddr::V4(v4) => self.ipv4_addresses.push(v4),
253 IpAddr::V6(v6) => self.ipv6_addresses.push(v6),
254 }
255 Ok(self)
256 }
257
258 /// Add multiple IP addresses
259 ///
260 /// # Errors
261 ///
262 /// Returns an error if any IP address string is invalid
263 ///
264 /// # Examples
265 ///
266 /// ```rust
267 /// use huginn_net_http::IpFilter;
268 ///
269 /// let filter = IpFilter::new()
270 /// .allow_list(vec!["8.8.8.8", "1.1.1.1", "2001:4860:4860::8888"])
271 /// .unwrap();
272 /// ```
273 pub fn allow_list(mut self, ips: Vec<&str>) -> Result<Self, String> {
274 for ip in ips {
275 self = self.allow(ip)?;
276 }
277 Ok(self)
278 }
279
280 /// Only check source addresses
281 ///
282 /// By default, both source and destination are checked.
283 pub fn source_only(mut self) -> Self {
284 self.check_source = true;
285 self.check_destination = false;
286 self
287 }
288
289 /// Only check destination addresses
290 ///
291 /// By default, both source and destination are checked.
292 pub fn destination_only(mut self) -> Self {
293 self.check_source = false;
294 self.check_destination = true;
295 self
296 }
297
298 /// Check if packet matches IP filter
299 ///
300 /// # Returns
301 ///
302 /// `true` if either source or destination IP matches (if enabled)
303 pub fn matches(&self, src_ip: &IpAddr, dst_ip: &IpAddr) -> bool {
304 let src_match = if self.check_source {
305 match src_ip {
306 IpAddr::V4(v4) => self.ipv4_addresses.contains(v4),
307 IpAddr::V6(v6) => self.ipv6_addresses.contains(v6),
308 }
309 } else {
310 false
311 };
312
313 let dst_match = if self.check_destination {
314 match dst_ip {
315 IpAddr::V4(v4) => self.ipv4_addresses.contains(v4),
316 IpAddr::V6(v6) => self.ipv6_addresses.contains(v6),
317 }
318 } else {
319 false
320 };
321
322 src_match || dst_match
323 }
324}
325
326/// Subnet filter configuration (CIDR notation)
327///
328/// Filters packets based on subnet membership using CIDR notation.
329/// Supports both IPv4 and IPv6 subnets.
330///
331/// # Examples
332///
333/// ```rust
334/// use huginn_net_http::SubnetFilter;
335///
336/// // Allow only private networks
337/// let filter = SubnetFilter::new()
338/// .allow("192.168.0.0/16").unwrap()
339/// .allow("10.0.0.0/8").unwrap();
340///
341/// // IPv6 subnet
342/// let filter = SubnetFilter::new()
343/// .allow("2001:db8::/32").unwrap();
344/// ```
345#[derive(Debug, Clone, Default)]
346pub struct SubnetFilter {
347 /// IPv4 subnets to match
348 pub ipv4_subnets: Vec<Ipv4Network>,
349 /// IPv6 subnets to match
350 pub ipv6_subnets: Vec<Ipv6Network>,
351 /// Check source, destination, or both?
352 pub check_source: bool,
353 pub check_destination: bool,
354}
355
356impl SubnetFilter {
357 /// Create a new subnet filter that checks both source and destination by default
358 pub fn new() -> Self {
359 Self { check_source: true, check_destination: true, ..Default::default() }
360 }
361
362 /// Add a subnet in CIDR notation
363 ///
364 /// # Errors
365 ///
366 /// Returns an error if the CIDR notation is invalid
367 ///
368 /// # Examples
369 ///
370 /// ```rust
371 /// use huginn_net_http::SubnetFilter;
372 ///
373 /// let filter = SubnetFilter::new()
374 /// .allow("192.168.1.0/24").unwrap();
375 /// ```
376 pub fn allow(mut self, cidr: &str) -> Result<Self, String> {
377 let network: IpNetwork = cidr.parse().map_err(|e| format!("Invalid CIDR: {e}"))?;
378 match network {
379 IpNetwork::V4(v4) => self.ipv4_subnets.push(v4),
380 IpNetwork::V6(v6) => self.ipv6_subnets.push(v6),
381 }
382 Ok(self)
383 }
384
385 /// Add multiple subnets
386 ///
387 /// # Errors
388 ///
389 /// Returns an error if any CIDR notation is invalid
390 ///
391 /// # Examples
392 ///
393 /// ```rust
394 /// use huginn_net_http::SubnetFilter;
395 ///
396 /// let filter = SubnetFilter::new()
397 /// .allow_list(vec!["192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"])
398 /// .unwrap();
399 /// ```
400 pub fn allow_list(mut self, cidrs: Vec<&str>) -> Result<Self, String> {
401 for cidr in cidrs {
402 self = self.allow(cidr)?;
403 }
404 Ok(self)
405 }
406
407 /// Only check source addresses
408 ///
409 /// By default, both source and destination are checked.
410 pub fn source_only(mut self) -> Self {
411 self.check_source = true;
412 self.check_destination = false;
413 self
414 }
415
416 /// Only check destination addresses
417 ///
418 /// By default, both source and destination are checked.
419 pub fn destination_only(mut self) -> Self {
420 self.check_source = false;
421 self.check_destination = true;
422 self
423 }
424
425 /// Check if packet matches subnet filter
426 ///
427 /// # Returns
428 ///
429 /// `true` if either source or destination IP is in any of the subnets (if enabled)
430 pub fn matches(&self, src_ip: &IpAddr, dst_ip: &IpAddr) -> bool {
431 let src_match = if self.check_source {
432 match src_ip {
433 IpAddr::V4(v4) => self.ipv4_subnets.iter().any(|net| net.contains(*v4)),
434 IpAddr::V6(v6) => self.ipv6_subnets.iter().any(|net| net.contains(*v6)),
435 }
436 } else {
437 false
438 };
439
440 let dst_match = if self.check_destination {
441 match dst_ip {
442 IpAddr::V4(v4) => self.ipv4_subnets.iter().any(|net| net.contains(*v4)),
443 IpAddr::V6(v6) => self.ipv6_subnets.iter().any(|net| net.contains(*v6)),
444 }
445 } else {
446 false
447 };
448
449 src_match || dst_match
450 }
451}
452
453/// Combined filter configuration
454///
455/// Combines port, IP, and subnet filters with a filter mode (Allow/Deny).
456/// All enabled filters must pass for a packet to be processed.
457///
458/// # Examples
459///
460/// ```rust
461/// use huginn_net_http::{FilterConfig, FilterMode, PortFilter, SubnetFilter};
462///
463/// let filter = FilterConfig::new()
464/// .mode(FilterMode::Allow)
465/// .with_port_filter(PortFilter::new().destination(443))
466/// .with_subnet_filter(
467/// SubnetFilter::new()
468/// .allow("192.168.0.0/16")
469/// .unwrap()
470/// );
471/// ```
472#[derive(Debug, Clone, Default)]
473pub struct FilterConfig {
474 pub port_filter: Option<PortFilter>,
475 pub ip_filter: Option<IpFilter>,
476 pub subnet_filter: Option<SubnetFilter>,
477 pub mode: FilterMode,
478}
479
480impl FilterConfig {
481 /// Create a new empty filter configuration
482 pub fn new() -> Self {
483 Self::default()
484 }
485
486 /// Set filter mode (Allow/Deny)
487 ///
488 /// # Examples
489 ///
490 /// ```
491 /// use huginn_net_http::{FilterConfig, FilterMode};
492 ///
493 /// // Allowlist mode (default) - only matching packets pass
494 /// let filter = FilterConfig::new().mode(FilterMode::Allow);
495 ///
496 /// // Denylist mode - matching packets are blocked
497 /// let filter = FilterConfig::new().mode(FilterMode::Deny);
498 /// ```
499 pub fn mode(mut self, mode: FilterMode) -> Self {
500 self.mode = mode;
501 self
502 }
503
504 /// Add port filter
505 ///
506 /// # Examples
507 ///
508 /// ```rust
509 /// use huginn_net_http::{FilterConfig, PortFilter};
510 ///
511 /// let filter = FilterConfig::new()
512 /// .with_port_filter(PortFilter::new().destination(443));
513 /// ```
514 pub fn with_port_filter(mut self, filter: PortFilter) -> Self {
515 self.port_filter = Some(filter);
516 self
517 }
518
519 /// Add IP filter
520 ///
521 /// # Examples
522 ///
523 /// ```rust
524 /// use huginn_net_http::{FilterConfig, IpFilter};
525 ///
526 /// let filter = FilterConfig::new()
527 /// .with_ip_filter(
528 /// IpFilter::new()
529 /// .allow("8.8.8.8")
530 /// .unwrap()
531 /// );
532 /// ```
533 pub fn with_ip_filter(mut self, filter: IpFilter) -> Self {
534 self.ip_filter = Some(filter);
535 self
536 }
537
538 /// Add subnet filter
539 ///
540 /// # Examples
541 ///
542 /// ```rust
543 /// use huginn_net_http::{FilterConfig, SubnetFilter};
544 ///
545 /// let filter = FilterConfig::new()
546 /// .with_subnet_filter(
547 /// SubnetFilter::new()
548 /// .allow("192.168.0.0/16")
549 /// .unwrap()
550 /// );
551 /// ```
552 pub fn with_subnet_filter(mut self, filter: SubnetFilter) -> Self {
553 self.subnet_filter = Some(filter);
554 self
555 }
556
557 /// Check if packet should be processed based on filters (userspace filtering)
558 ///
559 /// This method performs filtering in userspace after packets reach the application.
560 /// It extracts IP addresses and ports from packet headers and applies the configured
561 /// filters (port, IP, subnet) according to the filter mode (Allow/Deny).
562 ///
563 /// # Returns
564 ///
565 /// - `true`: Packet passes all filters (should be processed)
566 /// - `false`: Packet blocked by filters (should be dropped)
567 ///
568 /// # Logic
569 ///
570 /// - If no filters are configured, all packets pass
571 /// - In Allow mode: packet must match ALL configured filters
572 /// - In Deny mode: packet must NOT match ALL configured filters
573 pub fn should_process(
574 &self,
575 src_ip: &IpAddr,
576 dst_ip: &IpAddr,
577 src_port: u16,
578 dst_port: u16,
579 ) -> bool {
580 if self.port_filter.is_none() && self.ip_filter.is_none() && self.subnet_filter.is_none() {
581 return true;
582 }
583
584 match self.mode {
585 FilterMode::Allow => {
586 if let Some(ref filter) = self.port_filter {
587 if !filter.matches(src_port, dst_port) {
588 return false;
589 }
590 }
591
592 if let Some(ref filter) = self.ip_filter {
593 if !filter.matches(src_ip, dst_ip) {
594 return false;
595 }
596 }
597
598 if let Some(ref filter) = self.subnet_filter {
599 if !filter.matches(src_ip, dst_ip) {
600 return false;
601 }
602 }
603
604 true
605 }
606 FilterMode::Deny => {
607 let mut all_match = true;
608
609 if let Some(ref filter) = self.port_filter {
610 all_match = all_match && filter.matches(src_port, dst_port);
611 }
612
613 if let Some(ref filter) = self.ip_filter {
614 all_match = all_match && filter.matches(src_ip, dst_ip);
615 }
616
617 if let Some(ref filter) = self.subnet_filter {
618 all_match = all_match && filter.matches(src_ip, dst_ip);
619 }
620
621 !all_match
622 }
623 }
624 }
625}