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}