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