Skip to main content

windows_wfp/
filter.rs

1//! WFP filter rule definition
2//!
3//! Platform-specific filter rule that maps directly to WFP concepts.
4//! This is the main input type for [`FilterBuilder::add_filter`](crate::FilterBuilder::add_filter).
5
6use crate::condition::{IpAddrMask, Protocol};
7use crate::layer::FilterWeight;
8use std::path::PathBuf;
9
10/// Direction of network traffic
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Direction {
13    /// Traffic coming from the network to the local machine
14    Inbound,
15    /// Traffic initiated by the local machine going out
16    Outbound,
17}
18
19/// Action to take when a filter matches
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Action {
22    /// Allow the traffic through
23    Permit,
24    /// Block the traffic
25    Block,
26}
27
28/// A WFP filter rule definition
29///
30/// Describes a firewall filter to be applied via the Windows Filtering Platform.
31/// All condition fields are optional — omitting a condition means "match all" for that field.
32///
33/// # Examples
34///
35/// ```
36/// use windows_wfp::{FilterRule, Direction, Action, FilterWeight};
37/// use std::path::PathBuf;
38///
39/// // Block all outbound traffic from curl.exe
40/// let rule = FilterRule::new("Block curl", Direction::Outbound, Action::Block)
41///     .with_weight(FilterWeight::UserBlock)
42///     .with_app_path(r"C:\Windows\System32\curl.exe");
43///
44/// // Allow all outbound traffic (no conditions = match all)
45/// let allow_all = FilterRule::new("Allow all", Direction::Outbound, Action::Permit)
46///     .with_weight(FilterWeight::DefaultPermit);
47/// ```
48#[derive(Debug, Clone)]
49pub struct FilterRule {
50    /// Human-readable rule name (displayed in WFP management tools)
51    pub name: String,
52    /// Traffic direction
53    pub direction: Direction,
54    /// Action to take (permit or block)
55    pub action: Action,
56    /// Filter priority (higher weight = evaluated first)
57    pub weight: u64,
58    /// Application executable path (auto-converted to NT kernel path)
59    pub app_path: Option<PathBuf>,
60    /// Windows service name (matched via service SID)
61    pub service_name: Option<String>,
62    /// AppContainer SID (for UWP/packaged apps)
63    pub app_container_sid: Option<String>,
64    /// Local IP address with CIDR mask
65    pub local_ip: Option<IpAddrMask>,
66    /// Remote IP address with CIDR mask
67    pub remote_ip: Option<IpAddrMask>,
68    /// Local port number (1-65535)
69    pub local_port: Option<u16>,
70    /// Remote port number (1-65535)
71    pub remote_port: Option<u16>,
72    /// IP protocol (TCP, UDP, ICMP, etc.)
73    pub protocol: Option<Protocol>,
74}
75
76impl FilterRule {
77    /// Create a new filter rule with required fields
78    pub fn new(name: impl Into<String>, direction: Direction, action: Action) -> Self {
79        Self {
80            name: name.into(),
81            direction,
82            action,
83            weight: FilterWeight::UserPermit.value(),
84            app_path: None,
85            service_name: None,
86            app_container_sid: None,
87            local_ip: None,
88            remote_ip: None,
89            local_port: None,
90            remote_port: None,
91            protocol: None,
92        }
93    }
94
95    /// Set the filter weight (priority)
96    pub fn with_weight(mut self, weight: FilterWeight) -> Self {
97        self.weight = weight.value();
98        self
99    }
100
101    /// Set a raw weight value
102    pub fn with_raw_weight(mut self, weight: u64) -> Self {
103        self.weight = weight;
104        self
105    }
106
107    /// Set the application path to filter
108    pub fn with_app_path(mut self, path: impl Into<PathBuf>) -> Self {
109        self.app_path = Some(path.into());
110        self
111    }
112
113    /// Set the protocol to filter
114    pub fn with_protocol(mut self, protocol: Protocol) -> Self {
115        self.protocol = Some(protocol);
116        self
117    }
118
119    /// Set the remote port to filter
120    pub fn with_remote_port(mut self, port: u16) -> Self {
121        self.remote_port = Some(port);
122        self
123    }
124
125    /// Set the local port to filter
126    pub fn with_local_port(mut self, port: u16) -> Self {
127        self.local_port = Some(port);
128        self
129    }
130
131    /// Set the remote IP address with CIDR mask
132    pub fn with_remote_ip(mut self, ip: IpAddrMask) -> Self {
133        self.remote_ip = Some(ip);
134        self
135    }
136
137    /// Set the local IP address with CIDR mask
138    pub fn with_local_ip(mut self, ip: IpAddrMask) -> Self {
139        self.local_ip = Some(ip);
140        self
141    }
142
143    /// Set the Windows service name
144    pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
145        self.service_name = Some(name.into());
146        self
147    }
148
149    /// Set the AppContainer SID
150    pub fn with_app_container_sid(mut self, sid: impl Into<String>) -> Self {
151        self.app_container_sid = Some(sid.into());
152        self
153    }
154
155    /// Block all outbound traffic (no conditions)
156    pub fn block_all_outbound() -> Self {
157        Self::new("Block All Outbound", Direction::Outbound, Action::Block)
158            .with_weight(FilterWeight::Blocklist)
159    }
160
161    /// Allow all outbound traffic (no conditions)
162    pub fn allow_all_outbound() -> Self {
163        Self::new("Allow All Outbound", Direction::Outbound, Action::Permit)
164            .with_weight(FilterWeight::DefaultPermit)
165    }
166
167    /// Block all inbound traffic (no conditions)
168    pub fn block_all_inbound() -> Self {
169        Self::new("Block All Inbound", Direction::Inbound, Action::Block)
170            .with_weight(FilterWeight::DefaultBlock)
171    }
172
173    /// Default block rule (lowest priority catch-all)
174    pub fn default_block() -> Self {
175        Self::new("Default Block", Direction::Outbound, Action::Block)
176            .with_weight(FilterWeight::DefaultBlock)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_new_filter_rule_defaults() {
186        let rule = FilterRule::new("Test", Direction::Outbound, Action::Block);
187        assert_eq!(rule.name, "Test");
188        assert_eq!(rule.direction, Direction::Outbound);
189        assert_eq!(rule.action, Action::Block);
190        assert_eq!(rule.weight, FilterWeight::UserPermit.value());
191        assert!(rule.app_path.is_none());
192        assert!(rule.protocol.is_none());
193    }
194
195    #[test]
196    fn test_builder_pattern() {
197        let rule = FilterRule::new("Block curl", Direction::Outbound, Action::Block)
198            .with_weight(FilterWeight::UserBlock)
199            .with_app_path(r"C:\Windows\System32\curl.exe")
200            .with_protocol(Protocol::Tcp)
201            .with_remote_port(443);
202
203        assert_eq!(rule.weight, FilterWeight::UserBlock.value());
204        assert_eq!(rule.protocol, Some(Protocol::Tcp));
205        assert_eq!(rule.remote_port, Some(443));
206    }
207
208    #[test]
209    fn test_convenience_constructors() {
210        let block = FilterRule::block_all_outbound();
211        assert_eq!(block.action, Action::Block);
212        assert_eq!(block.weight, FilterWeight::Blocklist.value());
213
214        let allow = FilterRule::allow_all_outbound();
215        assert_eq!(allow.action, Action::Permit);
216
217        let default = FilterRule::default_block();
218        assert_eq!(default.weight, FilterWeight::DefaultBlock.value());
219    }
220
221    #[test]
222    fn test_with_raw_weight() {
223        let rule = FilterRule::new("Custom", Direction::Outbound, Action::Permit)
224            .with_raw_weight(42_000_000);
225        assert_eq!(rule.weight, 42_000_000);
226    }
227
228    #[test]
229    fn test_with_ip_conditions() {
230        use std::net::IpAddr;
231        let rule = FilterRule::new("IP filter", Direction::Outbound, Action::Block)
232            .with_remote_ip(IpAddrMask::new(
233                "192.168.1.0".parse::<IpAddr>().unwrap(),
234                24,
235            ))
236            .with_local_ip(IpAddrMask::new("10.0.0.1".parse::<IpAddr>().unwrap(), 32));
237
238        assert!(rule.remote_ip.is_some());
239        assert_eq!(rule.remote_ip.as_ref().unwrap().prefix_len, 24);
240    }
241
242    #[test]
243    fn test_with_service_name() {
244        let rule = FilterRule::new("Svc filter", Direction::Outbound, Action::Permit)
245            .with_service_name("dnscache");
246        assert_eq!(rule.service_name.as_deref(), Some("dnscache"));
247    }
248
249    #[test]
250    fn test_with_app_container_sid() {
251        let rule = FilterRule::new("UWP filter", Direction::Outbound, Action::Permit)
252            .with_app_container_sid("S-1-15-2-1234");
253        assert_eq!(rule.app_container_sid.as_deref(), Some("S-1-15-2-1234"));
254    }
255
256    #[test]
257    fn test_with_local_port() {
258        let rule = FilterRule::new("Port filter", Direction::Inbound, Action::Permit)
259            .with_local_port(8080);
260        assert_eq!(rule.local_port, Some(8080));
261    }
262
263    #[test]
264    fn test_block_all_inbound() {
265        let rule = FilterRule::block_all_inbound();
266        assert_eq!(rule.direction, Direction::Inbound);
267        assert_eq!(rule.action, Action::Block);
268        assert_eq!(rule.weight, FilterWeight::DefaultBlock.value());
269    }
270
271    #[test]
272    fn test_all_defaults_none() {
273        let rule = FilterRule::new("Empty", Direction::Outbound, Action::Permit);
274        assert!(rule.app_path.is_none());
275        assert!(rule.service_name.is_none());
276        assert!(rule.app_container_sid.is_none());
277        assert!(rule.local_ip.is_none());
278        assert!(rule.remote_ip.is_none());
279        assert!(rule.local_port.is_none());
280        assert!(rule.remote_port.is_none());
281        assert!(rule.protocol.is_none());
282    }
283
284    #[test]
285    fn test_full_builder_chain() {
286        use std::net::IpAddr;
287        let rule = FilterRule::new("Full", Direction::Outbound, Action::Block)
288            .with_weight(FilterWeight::UserBlock)
289            .with_app_path(r"C:\test.exe")
290            .with_protocol(Protocol::Tcp)
291            .with_remote_port(443)
292            .with_local_port(0)
293            .with_remote_ip(IpAddrMask::new("1.1.1.1".parse::<IpAddr>().unwrap(), 32))
294            .with_local_ip(IpAddrMask::new("10.0.0.1".parse::<IpAddr>().unwrap(), 32))
295            .with_service_name("svc")
296            .with_app_container_sid("sid");
297
298        assert_eq!(rule.name, "Full");
299        assert!(rule.app_path.is_some());
300        assert_eq!(rule.protocol, Some(Protocol::Tcp));
301        assert_eq!(rule.remote_port, Some(443));
302        assert_eq!(rule.local_port, Some(0));
303        assert!(rule.remote_ip.is_some());
304        assert!(rule.local_ip.is_some());
305        assert_eq!(rule.service_name.as_deref(), Some("svc"));
306        assert_eq!(rule.app_container_sid.as_deref(), Some("sid"));
307    }
308
309    #[test]
310    fn test_direction_copy_eq() {
311        let d1 = Direction::Outbound;
312        let d2 = d1; // Copy
313        assert_eq!(d1, d2);
314        assert_ne!(Direction::Inbound, Direction::Outbound);
315    }
316
317    #[test]
318    fn test_action_copy_eq() {
319        let a1 = Action::Permit;
320        let a2 = a1; // Copy
321        assert_eq!(a1, a2);
322        assert_ne!(Action::Permit, Action::Block);
323    }
324}