Skip to main content

windows_wfp/
filter_builder.rs

1//! WFP Filter translation from FilterRule
2//!
3//! Translates FilterRule into WFP FWPM_FILTER0 structures.
4//!
5//! # Path Format Conversion
6//!
7//! **CRITICAL**: WFP operates at the Windows kernel level and requires NT kernel paths,
8//! not DOS paths. This module automatically converts DOS paths to NT kernel format using
9//! the `FwpmGetAppIdFromFileName0` API.
10//!
11//! ## Why This Matters
12//!
13//! - **DOS path**: `C:\Windows\System32\curl.exe` (user-friendly format)
14//! - **NT kernel path**: `\device\harddiskvolume4\windows\system32\curl.exe` (kernel format)
15//!
16//! When a process makes a network connection, WFP identifies it using the NT kernel path.
17//! If your filter uses a DOS path, it will be added successfully but will **never match**
18//! any traffic because the path comparison fails at the kernel level.
19//!
20//! ## Implementation
21//!
22//! The `add_filter()` method automatically handles this conversion:
23//! 1. Takes a DOS path from `FilterRule.app_path` (e.g., `PathBuf::from(r"C:\Windows\System32\curl.exe")`)
24//! 2. Calls `FwpmGetAppIdFromFileName0` to convert it to NT kernel format
25//! 3. Uses the converted path in the WFP filter condition
26//! 4. Properly frees the allocated memory after filter creation
27//!
28//! This ensures filters work correctly without requiring users to know about NT kernel paths.
29
30use crate::condition::Protocol;
31use crate::constants::*;
32use crate::engine::WfpEngine;
33use crate::errors::{WfpError, WfpResult};
34use crate::filter::{Action, FilterRule};
35use crate::layer;
36use std::net::IpAddr;
37use std::ptr;
38use windows::core::{GUID, PWSTR};
39use windows::Win32::Foundation::ERROR_SUCCESS;
40use windows::Win32::NetworkManagement::WindowsFilteringPlatform::{
41    FwpmFilterAdd0, FwpmFilterDeleteById0, FwpmFreeMemory0, FwpmGetAppIdFromFileName0,
42    FWPM_FILTER0, FWPM_FILTER_CONDITION0, FWPM_FILTER_FLAGS, FWP_ACTION_BLOCK, FWP_ACTION_PERMIT,
43    FWP_ACTION_TYPE, FWP_BYTE_BLOB, FWP_BYTE_BLOB_TYPE, FWP_CONDITION_VALUE0, FWP_MATCH_EQUAL,
44    FWP_UINT16, FWP_UINT64, FWP_UINT8, FWP_V4_ADDR_AND_MASK, FWP_V4_ADDR_MASK,
45    FWP_V6_ADDR_AND_MASK, FWP_V6_ADDR_MASK,
46};
47
48/// WFP Filter builder
49///
50/// Translates [`FilterRule`] into WFP filter structures and manages filter lifecycle.
51///
52/// # Examples
53///
54/// ```no_run
55/// use windows_wfp::{WfpEngine, FilterBuilder, FilterRule, Direction, Action, FilterWeight, initialize_wfp};
56///
57/// let engine = WfpEngine::new()?;
58/// initialize_wfp(&engine)?;
59///
60/// let rule = FilterRule::new("Block curl", Direction::Outbound, Action::Block)
61///     .with_weight(FilterWeight::UserBlock)
62///     .with_app_path(r"C:\Windows\System32\curl.exe");
63///
64/// let filter_id = FilterBuilder::add_filter(&engine, &rule)?;
65/// // Later: remove the filter
66/// FilterBuilder::delete_filter(&engine, filter_id)?;
67/// # Ok::<(), windows_wfp::WfpError>(())
68/// ```
69pub struct FilterBuilder;
70
71impl FilterBuilder {
72    /// Translate Action to WFP action type
73    fn translate_action(action: Action) -> FWP_ACTION_TYPE {
74        match action {
75            Action::Permit => FWP_ACTION_PERMIT,
76            Action::Block => FWP_ACTION_BLOCK,
77        }
78    }
79
80    /// Translate Protocol to IP protocol number
81    fn translate_protocol(protocol: Protocol) -> u8 {
82        protocol.as_u8()
83    }
84
85    /// Convert CIDR prefix length to IPv4 netmask
86    ///
87    /// Example: prefix_len=24 → 0xFFFFFF00 (255.255.255.0)
88    fn prefix_to_v4_mask(prefix_len: u8) -> u32 {
89        if prefix_len == 0 {
90            0
91        } else if prefix_len >= 32 {
92            0xFFFFFFFF
93        } else {
94            u32::MAX << (32 - prefix_len)
95        }
96    }
97
98    /// Add filter to WFP engine
99    ///
100    /// Translates a [`FilterRule`] and adds it to the WFP engine.
101    ///
102    /// # Errors
103    ///
104    /// Returns `WfpError::FilterAddFailed` if the filter cannot be added.
105    ///
106    /// # Examples
107    ///
108    /// ```no_run
109    /// use windows_wfp::{WfpEngine, FilterBuilder, FilterRule, Direction, Action, FilterWeight, initialize_wfp};
110    ///
111    /// let engine = WfpEngine::new()?;
112    /// initialize_wfp(&engine)?;
113    ///
114    /// let rule = FilterRule::new("Allow all outbound", Direction::Outbound, Action::Permit)
115    ///     .with_weight(FilterWeight::DefaultPermit);
116    /// let filter_id = FilterBuilder::add_filter(&engine, &rule)?;
117    /// # Ok::<(), windows_wfp::WfpError>(())
118    /// ```
119    pub fn add_filter(engine: &WfpEngine, rule: &FilterRule) -> WfpResult<u64> {
120        // Determine if rule uses IPv6 (check both remote_ip and local_ip)
121        let is_ipv6 = rule
122            .remote_ip
123            .as_ref()
124            .map(|ip| ip.is_ipv6())
125            .or_else(|| rule.local_ip.as_ref().map(|ip| ip.is_ipv6()))
126            .unwrap_or(false);
127
128        let layer_key = layer::select_layer(rule.direction, is_ipv6);
129        let action = Self::translate_action(rule.action);
130
131        // Convert name to wide string - must outlive FwpmFilterAdd0 call
132        let name_wide: Vec<u16> = rule.name.encode_utf16().chain(std::iter::once(0)).collect();
133
134        // Weight storage - must outlive FwpmFilterAdd0 call
135        let weight_value: u64 = rule.weight;
136
137        // CRITICAL: Convert DOS path to NT kernel format using FwpmGetAppIdFromFileName0.
138        // If the conversion fails (e.g. file not found), return an error instead of silently
139        // skipping the condition, which would cause the filter to match ALL applications.
140        let app_id_blob: Option<*mut FWP_BYTE_BLOB> = if let Some(app_path) = &rule.app_path {
141            let path_str = app_path.to_string_lossy().to_string();
142            // path_wide must remain alive for the duration of the API call
143            let path_wide: Vec<u16> = path_str.encode_utf16().chain(std::iter::once(0)).collect();
144
145            let mut blob_ptr: *mut FWP_BYTE_BLOB = ptr::null_mut();
146            let result = unsafe {
147                FwpmGetAppIdFromFileName0(PWSTR(path_wide.as_ptr() as *mut u16), &mut blob_ptr)
148            };
149
150            if result != ERROR_SUCCESS.0 {
151                return Err(WfpError::AppPathNotFound(path_str));
152            }
153
154            Some(blob_ptr)
155        } else {
156            None
157        };
158
159        let remote_v4_mask: Option<FWP_V4_ADDR_AND_MASK> =
160            rule.remote_ip.as_ref().and_then(|remote_ip| {
161                if let IpAddr::V4(ipv4) = remote_ip.addr {
162                    Some(FWP_V4_ADDR_AND_MASK {
163                        addr: u32::from_be_bytes(ipv4.octets()),
164                        mask: Self::prefix_to_v4_mask(remote_ip.prefix_len),
165                    })
166                } else {
167                    None
168                }
169            });
170
171        let remote_v6_mask: Option<FWP_V6_ADDR_AND_MASK> =
172            rule.remote_ip.as_ref().and_then(|remote_ip| {
173                if let IpAddr::V6(ipv6) = remote_ip.addr {
174                    Some(FWP_V6_ADDR_AND_MASK {
175                        addr: ipv6.octets(),
176                        prefixLength: remote_ip.prefix_len,
177                    })
178                } else {
179                    None
180                }
181            });
182
183        let local_v4_mask: Option<FWP_V4_ADDR_AND_MASK> =
184            rule.local_ip.as_ref().and_then(|local_ip| {
185                if let IpAddr::V4(ipv4) = local_ip.addr {
186                    Some(FWP_V4_ADDR_AND_MASK {
187                        addr: u32::from_be_bytes(ipv4.octets()),
188                        mask: Self::prefix_to_v4_mask(local_ip.prefix_len),
189                    })
190                } else {
191                    None
192                }
193            });
194
195        let local_v6_mask: Option<FWP_V6_ADDR_AND_MASK> =
196            rule.local_ip.as_ref().and_then(|local_ip| {
197                if let IpAddr::V6(ipv6) = local_ip.addr {
198                    Some(FWP_V6_ADDR_AND_MASK {
199                        addr: ipv6.octets(),
200                        prefixLength: local_ip.prefix_len,
201                    })
202                } else {
203                    None
204                }
205            });
206
207        // Build conditions
208        let mut conditions = Vec::new();
209
210        // Condition: APP_ID (application path in NT kernel format)
211        if let Some(blob_ptr) = app_id_blob {
212            conditions.push(FWPM_FILTER_CONDITION0 {
213                fieldKey: CONDITION_ALE_APP_ID,
214                matchType: FWP_MATCH_EQUAL,
215                conditionValue: FWP_CONDITION_VALUE0 {
216                    r#type: FWP_BYTE_BLOB_TYPE,
217                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
218                        byteBlob: blob_ptr,
219                    },
220                },
221            });
222        }
223
224        // Condition: REMOTE_PORT
225        if let Some(remote_port) = rule.remote_port {
226            conditions.push(FWPM_FILTER_CONDITION0 {
227                fieldKey: CONDITION_IP_REMOTE_PORT,
228                matchType: FWP_MATCH_EQUAL,
229                conditionValue: FWP_CONDITION_VALUE0 {
230                    r#type: FWP_UINT16,
231                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
232                        uint16: remote_port,
233                    },
234                },
235            });
236        }
237
238        // Condition: LOCAL_PORT
239        if let Some(local_port) = rule.local_port {
240            conditions.push(FWPM_FILTER_CONDITION0 {
241                fieldKey: CONDITION_IP_LOCAL_PORT,
242                matchType: FWP_MATCH_EQUAL,
243                conditionValue: FWP_CONDITION_VALUE0 {
244                    r#type: FWP_UINT16,
245                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
246                        uint16: local_port,
247                    },
248                },
249            });
250        }
251
252        // Condition: PROTOCOL
253        if let Some(protocol) = rule.protocol {
254            conditions.push(FWPM_FILTER_CONDITION0 {
255                fieldKey: CONDITION_IP_PROTOCOL,
256                matchType: FWP_MATCH_EQUAL,
257                conditionValue: FWP_CONDITION_VALUE0 {
258                    r#type: FWP_UINT8,
259                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
260                        uint8: Self::translate_protocol(protocol),
261                    },
262                },
263            });
264        }
265
266        // Condition: REMOTE_IP (IPv4)
267        if let Some(ref mask) = remote_v4_mask {
268            conditions.push(FWPM_FILTER_CONDITION0 {
269                fieldKey: CONDITION_IP_REMOTE_ADDRESS,
270                matchType: FWP_MATCH_EQUAL,
271                conditionValue: FWP_CONDITION_VALUE0 {
272                    r#type: FWP_V4_ADDR_MASK,
273                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
274                        v4AddrMask: mask as *const _ as *mut _,
275                    },
276                },
277            });
278        }
279
280        // Condition: REMOTE_IP (IPv6)
281        if let Some(ref mask) = remote_v6_mask {
282            conditions.push(FWPM_FILTER_CONDITION0 {
283                fieldKey: CONDITION_IP_REMOTE_ADDRESS,
284                matchType: FWP_MATCH_EQUAL,
285                conditionValue: FWP_CONDITION_VALUE0 {
286                    r#type: FWP_V6_ADDR_MASK,
287                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
288                        v6AddrMask: mask as *const _ as *mut _,
289                    },
290                },
291            });
292        }
293
294        // Condition: LOCAL_IP (IPv4)
295        if let Some(ref mask) = local_v4_mask {
296            conditions.push(FWPM_FILTER_CONDITION0 {
297                fieldKey: CONDITION_IP_LOCAL_ADDRESS,
298                matchType: FWP_MATCH_EQUAL,
299                conditionValue: FWP_CONDITION_VALUE0 {
300                    r#type: FWP_V4_ADDR_MASK,
301                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
302                        v4AddrMask: mask as *const _ as *mut _,
303                    },
304                },
305            });
306        }
307
308        // Condition: LOCAL_IP (IPv6)
309        if let Some(ref mask) = local_v6_mask {
310            conditions.push(FWPM_FILTER_CONDITION0 {
311                fieldKey: CONDITION_IP_LOCAL_ADDRESS,
312                matchType: FWP_MATCH_EQUAL,
313                conditionValue: FWP_CONDITION_VALUE0 {
314                    r#type: FWP_V6_ADDR_MASK,
315                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
316                        v6AddrMask: mask as *const _ as *mut _,
317                    },
318                },
319            });
320        }
321
322        // Create the filter structure
323        let filter = FWPM_FILTER0 {
324            filterKey: GUID::zeroed(),
325            displayData:
326                windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_DISPLAY_DATA0 {
327                    name: PWSTR(name_wide.as_ptr() as *mut u16),
328                    description: PWSTR::null(),
329                },
330            flags: FWPM_FILTER_FLAGS(0),
331            providerKey: &WFP_PROVIDER_GUID as *const _ as *mut _,
332            providerData:
333                windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_BYTE_BLOB {
334                    size: 0,
335                    data: ptr::null_mut(),
336                },
337            layerKey: layer_key,
338            subLayerKey: WFP_SUBLAYER_GUID,
339            weight: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_VALUE0 {
340                r#type: FWP_UINT64,
341                Anonymous:
342                    windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_VALUE0_0 {
343                        uint64: &weight_value as *const u64 as *mut u64,
344                    },
345            },
346            numFilterConditions: conditions.len() as u32,
347            filterCondition: if conditions.is_empty() {
348                ptr::null_mut()
349            } else {
350                conditions.as_ptr() as *mut _
351            },
352            action: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_ACTION0 {
353                r#type: action,
354                Anonymous: Default::default(),
355            },
356            Anonymous: Default::default(),
357            reserved: ptr::null_mut(),
358            filterId: 0,
359            effectiveWeight: Default::default(),
360        };
361
362        let mut filter_id: u64 = 0;
363
364        unsafe {
365            let result = FwpmFilterAdd0(engine.handle(), &filter, None, Some(&mut filter_id));
366
367            // Free memory allocated by FwpmGetAppIdFromFileName0 regardless of add result
368            if let Some(mut blob_ptr) = app_id_blob {
369                if !blob_ptr.is_null() {
370                    FwpmFreeMemory0(&mut blob_ptr as *mut _ as *mut *mut _);
371                }
372            }
373
374            if result != ERROR_SUCCESS.0 {
375                return Err(WfpError::FilterAddFailed(format!(
376                    "Failed to add filter '{}': error code {}",
377                    rule.name, result
378                )));
379            }
380        }
381
382        Ok(filter_id)
383    }
384
385    /// Delete filter from WFP engine by ID
386    ///
387    /// Removes a previously added filter using its unique ID.
388    ///
389    /// # Errors
390    ///
391    /// Returns `WfpError::FilterDeleteFailed` if the filter cannot be deleted.
392    ///
393    /// # Examples
394    ///
395    /// ```no_run
396    /// use windows_wfp::{WfpEngine, FilterBuilder, FilterRule, Direction, Action, FilterWeight, initialize_wfp};
397    ///
398    /// let engine = WfpEngine::new()?;
399    /// initialize_wfp(&engine)?;
400    ///
401    /// let rule = FilterRule::new("Allow all", Direction::Outbound, Action::Permit)
402    ///     .with_weight(FilterWeight::DefaultPermit);
403    /// let filter_id = FilterBuilder::add_filter(&engine, &rule)?;
404    ///
405    /// // Later: remove the filter
406    /// FilterBuilder::delete_filter(&engine, filter_id)?;
407    /// # Ok::<(), windows_wfp::WfpError>(())
408    /// ```
409    pub fn delete_filter(engine: &WfpEngine, filter_id: u64) -> WfpResult<()> {
410        unsafe {
411            let result = FwpmFilterDeleteById0(engine.handle(), filter_id);
412
413            if result != ERROR_SUCCESS.0 {
414                return Err(WfpError::FilterDeleteFailed(format!(
415                    "Failed to delete filter ID {}: error code {}",
416                    filter_id, result
417                )));
418            }
419        }
420
421        Ok(())
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use crate::condition::Protocol;
429    use crate::filter::{Action, FilterRule};
430
431    #[test]
432    fn test_action_translation() {
433        assert_eq!(
434            FilterBuilder::translate_action(Action::Permit),
435            FWP_ACTION_PERMIT
436        );
437        assert_eq!(
438            FilterBuilder::translate_action(Action::Block),
439            FWP_ACTION_BLOCK
440        );
441    }
442
443    #[test]
444    fn test_protocol_translation() {
445        assert_eq!(FilterBuilder::translate_protocol(Protocol::Hopopt), 0);
446        assert_eq!(FilterBuilder::translate_protocol(Protocol::Icmp), 1);
447        assert_eq!(FilterBuilder::translate_protocol(Protocol::Igmp), 2);
448        assert_eq!(FilterBuilder::translate_protocol(Protocol::Tcp), 6);
449        assert_eq!(FilterBuilder::translate_protocol(Protocol::Udp), 17);
450        assert_eq!(FilterBuilder::translate_protocol(Protocol::Gre), 47);
451        assert_eq!(FilterBuilder::translate_protocol(Protocol::Esp), 50);
452        assert_eq!(FilterBuilder::translate_protocol(Protocol::Ah), 51);
453        assert_eq!(FilterBuilder::translate_protocol(Protocol::Icmpv6), 58);
454    }
455
456    #[test]
457    fn test_prefix_to_v4_mask() {
458        assert_eq!(FilterBuilder::prefix_to_v4_mask(0), 0x00000000);
459        assert_eq!(FilterBuilder::prefix_to_v4_mask(8), 0xFF000000);
460        assert_eq!(FilterBuilder::prefix_to_v4_mask(16), 0xFFFF0000);
461        assert_eq!(FilterBuilder::prefix_to_v4_mask(24), 0xFFFFFF00);
462        assert_eq!(FilterBuilder::prefix_to_v4_mask(32), 0xFFFFFFFF);
463    }
464
465    #[test]
466    fn test_prefix_to_v4_mask_all_values() {
467        assert_eq!(FilterBuilder::prefix_to_v4_mask(1), 0x80000000);
468        assert_eq!(FilterBuilder::prefix_to_v4_mask(4), 0xF0000000);
469        assert_eq!(FilterBuilder::prefix_to_v4_mask(12), 0xFFF00000);
470        assert_eq!(FilterBuilder::prefix_to_v4_mask(20), 0xFFFFF000);
471        assert_eq!(FilterBuilder::prefix_to_v4_mask(28), 0xFFFFFFF0);
472        assert_eq!(FilterBuilder::prefix_to_v4_mask(31), 0xFFFFFFFE);
473    }
474
475    #[test]
476    fn test_prefix_to_v4_mask_overflow() {
477        // prefix > 32 should saturate to all ones
478        assert_eq!(FilterBuilder::prefix_to_v4_mask(33), 0xFFFFFFFF);
479        assert_eq!(FilterBuilder::prefix_to_v4_mask(255), 0xFFFFFFFF);
480    }
481
482    #[test]
483    #[ignore] // Requires admin privileges
484    fn test_add_simple_filter() {
485        let engine = WfpEngine::new().expect("Failed to create engine");
486        crate::initialize_wfp(&engine).expect("Failed to initialize WFP");
487
488        let rule = FilterRule::allow_all_outbound();
489        let result = FilterBuilder::add_filter(&engine, &rule);
490
491        assert!(result.is_ok(), "Failed to add filter: {:?}", result);
492    }
493
494    #[test]
495    #[ignore] // Requires admin privileges
496    fn test_add_and_delete_filter() {
497        let engine = WfpEngine::new().expect("Failed to create engine");
498        crate::initialize_wfp(&engine).expect("Failed to initialize WFP");
499
500        let rule = FilterRule::allow_all_outbound();
501
502        let filter_id = FilterBuilder::add_filter(&engine, &rule).expect("Failed to add filter");
503        let result = FilterBuilder::delete_filter(&engine, filter_id);
504
505        assert!(result.is_ok(), "Failed to delete filter: {:?}", result);
506    }
507
508    #[test]
509    #[ignore] // Requires admin privileges
510    fn test_add_filter_with_nonexistent_app_path_returns_error() {
511        let engine = WfpEngine::new().expect("Failed to create engine");
512        crate::initialize_wfp(&engine).expect("Failed to initialize WFP");
513
514        let rule = FilterRule::new("Test", crate::Direction::Outbound, crate::Action::Block)
515            .with_app_path(r"C:\this\path\does\not\exist.exe");
516
517        let result = FilterBuilder::add_filter(&engine, &rule);
518        assert!(
519            matches!(result, Err(WfpError::AppPathNotFound(_))),
520            "Expected AppPathNotFound, got: {:?}",
521            result
522        );
523    }
524}