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 (based on remote_ip if present)
121        let is_ipv6 = rule
122            .remote_ip
123            .as_ref()
124            .map(|ip_mask| matches!(ip_mask.addr, IpAddr::V6(_)))
125            .unwrap_or(false);
126
127        let layer_key = layer::select_layer(rule.direction, is_ipv6);
128        let action = Self::translate_action(rule.action);
129
130        // Convert name to wide string - must outlive FwpmFilterAdd0 call
131        let name_wide: Vec<u16> = rule.name.encode_utf16().chain(std::iter::once(0)).collect();
132
133        // Weight storage - must outlive FwpmFilterAdd0 call
134        let weight_value: u64 = rule.weight;
135
136        // CRITICAL: Convert DOS path to NT kernel format using FwpmGetAppIdFromFileName0
137        let app_id_blob: Option<(*mut FWP_BYTE_BLOB, bool)> =
138            rule.app_path.as_ref().and_then(|app_path| unsafe {
139                let path_str = app_path.to_string_lossy().to_string();
140                let path_wide: Vec<u16> =
141                    path_str.encode_utf16().chain(std::iter::once(0)).collect();
142                let pwstr = PWSTR(path_wide.as_ptr() as *mut u16);
143
144                let mut blob_ptr: *mut FWP_BYTE_BLOB = ptr::null_mut();
145                let result = FwpmGetAppIdFromFileName0(pwstr, &mut blob_ptr);
146
147                if result == ERROR_SUCCESS.0 {
148                    Some((blob_ptr, true))
149                } else {
150                    None
151                }
152            });
153
154        let remote_v4_mask: Option<FWP_V4_ADDR_AND_MASK> =
155            rule.remote_ip.as_ref().and_then(|remote_ip| {
156                if let IpAddr::V4(ipv4) = remote_ip.addr {
157                    Some(FWP_V4_ADDR_AND_MASK {
158                        addr: u32::from_be_bytes(ipv4.octets()),
159                        mask: Self::prefix_to_v4_mask(remote_ip.prefix_len),
160                    })
161                } else {
162                    None
163                }
164            });
165
166        let remote_v6_mask: Option<FWP_V6_ADDR_AND_MASK> =
167            rule.remote_ip.as_ref().and_then(|remote_ip| {
168                if let IpAddr::V6(ipv6) = remote_ip.addr {
169                    Some(FWP_V6_ADDR_AND_MASK {
170                        addr: ipv6.octets(),
171                        prefixLength: remote_ip.prefix_len,
172                    })
173                } else {
174                    None
175                }
176            });
177
178        let local_v4_mask: Option<FWP_V4_ADDR_AND_MASK> =
179            rule.local_ip.as_ref().and_then(|local_ip| {
180                if let IpAddr::V4(ipv4) = local_ip.addr {
181                    Some(FWP_V4_ADDR_AND_MASK {
182                        addr: u32::from_be_bytes(ipv4.octets()),
183                        mask: Self::prefix_to_v4_mask(local_ip.prefix_len),
184                    })
185                } else {
186                    None
187                }
188            });
189
190        let local_v6_mask: Option<FWP_V6_ADDR_AND_MASK> =
191            rule.local_ip.as_ref().and_then(|local_ip| {
192                if let IpAddr::V6(ipv6) = local_ip.addr {
193                    Some(FWP_V6_ADDR_AND_MASK {
194                        addr: ipv6.octets(),
195                        prefixLength: local_ip.prefix_len,
196                    })
197                } else {
198                    None
199                }
200            });
201
202        // Build conditions
203        let mut conditions = Vec::new();
204
205        // Condition: APP_ID (application path in NT kernel format)
206        if let Some((blob_ptr, _)) = app_id_blob {
207            conditions.push(FWPM_FILTER_CONDITION0 {
208                fieldKey: CONDITION_ALE_APP_ID,
209                matchType: FWP_MATCH_EQUAL,
210                conditionValue: FWP_CONDITION_VALUE0 {
211                    r#type: FWP_BYTE_BLOB_TYPE,
212                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
213                        byteBlob: blob_ptr,
214                    },
215                },
216            });
217        }
218
219        // Condition: REMOTE_PORT
220        if let Some(remote_port) = rule.remote_port {
221            conditions.push(FWPM_FILTER_CONDITION0 {
222                fieldKey: CONDITION_IP_REMOTE_PORT,
223                matchType: FWP_MATCH_EQUAL,
224                conditionValue: FWP_CONDITION_VALUE0 {
225                    r#type: FWP_UINT16,
226                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
227                        uint16: remote_port,
228                    },
229                },
230            });
231        }
232
233        // Condition: LOCAL_PORT
234        if let Some(local_port) = rule.local_port {
235            conditions.push(FWPM_FILTER_CONDITION0 {
236                fieldKey: CONDITION_IP_LOCAL_PORT,
237                matchType: FWP_MATCH_EQUAL,
238                conditionValue: FWP_CONDITION_VALUE0 {
239                    r#type: FWP_UINT16,
240                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
241                        uint16: local_port,
242                    },
243                },
244            });
245        }
246
247        // Condition: PROTOCOL
248        if let Some(protocol) = rule.protocol {
249            conditions.push(FWPM_FILTER_CONDITION0 {
250                fieldKey: CONDITION_IP_PROTOCOL,
251                matchType: FWP_MATCH_EQUAL,
252                conditionValue: FWP_CONDITION_VALUE0 {
253                    r#type: FWP_UINT8,
254                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
255                        uint8: Self::translate_protocol(protocol),
256                    },
257                },
258            });
259        }
260
261        // Condition: REMOTE_IP (IPv4)
262        if let Some(ref mask) = remote_v4_mask {
263            conditions.push(FWPM_FILTER_CONDITION0 {
264                fieldKey: CONDITION_IP_REMOTE_ADDRESS,
265                matchType: FWP_MATCH_EQUAL,
266                conditionValue: FWP_CONDITION_VALUE0 {
267                    r#type: FWP_V4_ADDR_MASK,
268                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
269                        v4AddrMask: mask as *const _ as *mut _,
270                    },
271                },
272            });
273        }
274
275        // Condition: REMOTE_IP (IPv6)
276        if let Some(ref mask) = remote_v6_mask {
277            conditions.push(FWPM_FILTER_CONDITION0 {
278                fieldKey: CONDITION_IP_REMOTE_ADDRESS,
279                matchType: FWP_MATCH_EQUAL,
280                conditionValue: FWP_CONDITION_VALUE0 {
281                    r#type: FWP_V6_ADDR_MASK,
282                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
283                        v6AddrMask: mask as *const _ as *mut _,
284                    },
285                },
286            });
287        }
288
289        // Condition: LOCAL_IP (IPv4)
290        if let Some(ref mask) = local_v4_mask {
291            conditions.push(FWPM_FILTER_CONDITION0 {
292                fieldKey: CONDITION_IP_LOCAL_ADDRESS,
293                matchType: FWP_MATCH_EQUAL,
294                conditionValue: FWP_CONDITION_VALUE0 {
295                    r#type: FWP_V4_ADDR_MASK,
296                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
297                        v4AddrMask: mask as *const _ as *mut _,
298                    },
299                },
300            });
301        }
302
303        // Condition: LOCAL_IP (IPv6)
304        if let Some(ref mask) = local_v6_mask {
305            conditions.push(FWPM_FILTER_CONDITION0 {
306                fieldKey: CONDITION_IP_LOCAL_ADDRESS,
307                matchType: FWP_MATCH_EQUAL,
308                conditionValue: FWP_CONDITION_VALUE0 {
309                    r#type: FWP_V6_ADDR_MASK,
310                    Anonymous: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_CONDITION_VALUE0_0 {
311                        v6AddrMask: mask as *const _ as *mut _,
312                    },
313                },
314            });
315        }
316
317        // Create the filter structure
318        let filter = FWPM_FILTER0 {
319            filterKey: GUID::zeroed(),
320            displayData:
321                windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_DISPLAY_DATA0 {
322                    name: PWSTR(name_wide.as_ptr() as *mut u16),
323                    description: PWSTR::null(),
324                },
325            flags: FWPM_FILTER_FLAGS(0),
326            providerKey: &WFP_PROVIDER_GUID as *const _ as *mut _,
327            providerData:
328                windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_BYTE_BLOB {
329                    size: 0,
330                    data: ptr::null_mut(),
331                },
332            layerKey: layer_key,
333            subLayerKey: WFP_SUBLAYER_GUID,
334            weight: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_VALUE0 {
335                r#type: FWP_UINT64,
336                Anonymous:
337                    windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWP_VALUE0_0 {
338                        uint64: &weight_value as *const u64 as *mut u64,
339                    },
340            },
341            numFilterConditions: conditions.len() as u32,
342            filterCondition: if conditions.is_empty() {
343                ptr::null_mut()
344            } else {
345                conditions.as_ptr() as *mut _
346            },
347            action: windows::Win32::NetworkManagement::WindowsFilteringPlatform::FWPM_ACTION0 {
348                r#type: action,
349                Anonymous: Default::default(),
350            },
351            Anonymous: Default::default(),
352            reserved: ptr::null_mut(),
353            filterId: 0,
354            effectiveWeight: Default::default(),
355        };
356
357        let mut filter_id: u64 = 0;
358
359        unsafe {
360            let result = FwpmFilterAdd0(engine.handle(), &filter, None, Some(&mut filter_id));
361
362            // Free memory allocated by FwpmGetAppIdFromFileName0 regardless of add result
363            if let Some((mut blob_ptr, needs_free)) = app_id_blob {
364                if needs_free && !blob_ptr.is_null() {
365                    FwpmFreeMemory0(&mut blob_ptr as *mut _ as *mut *mut _);
366                }
367            }
368
369            if result != ERROR_SUCCESS.0 {
370                return Err(WfpError::FilterAddFailed(format!(
371                    "Failed to add filter '{}': error code {}",
372                    rule.name, result
373                )));
374            }
375        }
376
377        Ok(filter_id)
378    }
379
380    /// Delete filter from WFP engine by ID
381    ///
382    /// Removes a previously added filter using its unique ID.
383    ///
384    /// # Errors
385    ///
386    /// Returns `WfpError::FilterDeleteFailed` if the filter cannot be deleted.
387    ///
388    /// # Examples
389    ///
390    /// ```no_run
391    /// use windows_wfp::{WfpEngine, FilterBuilder, FilterRule, Direction, Action, FilterWeight, initialize_wfp};
392    ///
393    /// let engine = WfpEngine::new()?;
394    /// initialize_wfp(&engine)?;
395    ///
396    /// let rule = FilterRule::new("Allow all", Direction::Outbound, Action::Permit)
397    ///     .with_weight(FilterWeight::DefaultPermit);
398    /// let filter_id = FilterBuilder::add_filter(&engine, &rule)?;
399    ///
400    /// // Later: remove the filter
401    /// FilterBuilder::delete_filter(&engine, filter_id)?;
402    /// # Ok::<(), windows_wfp::WfpError>(())
403    /// ```
404    pub fn delete_filter(engine: &WfpEngine, filter_id: u64) -> WfpResult<()> {
405        unsafe {
406            let result = FwpmFilterDeleteById0(engine.handle(), filter_id);
407
408            if result != ERROR_SUCCESS.0 {
409                return Err(WfpError::FilterDeleteFailed(format!(
410                    "Failed to delete filter ID {}: error code {}",
411                    filter_id, result
412                )));
413            }
414        }
415
416        Ok(())
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::condition::Protocol;
424    use crate::filter::{Action, FilterRule};
425
426    #[test]
427    fn test_action_translation() {
428        assert_eq!(
429            FilterBuilder::translate_action(Action::Permit),
430            FWP_ACTION_PERMIT
431        );
432        assert_eq!(
433            FilterBuilder::translate_action(Action::Block),
434            FWP_ACTION_BLOCK
435        );
436    }
437
438    #[test]
439    fn test_protocol_translation() {
440        assert_eq!(FilterBuilder::translate_protocol(Protocol::Hopopt), 0);
441        assert_eq!(FilterBuilder::translate_protocol(Protocol::Icmp), 1);
442        assert_eq!(FilterBuilder::translate_protocol(Protocol::Igmp), 2);
443        assert_eq!(FilterBuilder::translate_protocol(Protocol::Tcp), 6);
444        assert_eq!(FilterBuilder::translate_protocol(Protocol::Udp), 17);
445        assert_eq!(FilterBuilder::translate_protocol(Protocol::Gre), 47);
446        assert_eq!(FilterBuilder::translate_protocol(Protocol::Esp), 50);
447        assert_eq!(FilterBuilder::translate_protocol(Protocol::Ah), 51);
448        assert_eq!(FilterBuilder::translate_protocol(Protocol::Icmpv6), 58);
449    }
450
451    #[test]
452    fn test_prefix_to_v4_mask() {
453        assert_eq!(FilterBuilder::prefix_to_v4_mask(0), 0x00000000);
454        assert_eq!(FilterBuilder::prefix_to_v4_mask(8), 0xFF000000);
455        assert_eq!(FilterBuilder::prefix_to_v4_mask(16), 0xFFFF0000);
456        assert_eq!(FilterBuilder::prefix_to_v4_mask(24), 0xFFFFFF00);
457        assert_eq!(FilterBuilder::prefix_to_v4_mask(32), 0xFFFFFFFF);
458    }
459
460    #[test]
461    fn test_prefix_to_v4_mask_all_values() {
462        assert_eq!(FilterBuilder::prefix_to_v4_mask(1), 0x80000000);
463        assert_eq!(FilterBuilder::prefix_to_v4_mask(4), 0xF0000000);
464        assert_eq!(FilterBuilder::prefix_to_v4_mask(12), 0xFFF00000);
465        assert_eq!(FilterBuilder::prefix_to_v4_mask(20), 0xFFFFF000);
466        assert_eq!(FilterBuilder::prefix_to_v4_mask(28), 0xFFFFFFF0);
467        assert_eq!(FilterBuilder::prefix_to_v4_mask(31), 0xFFFFFFFE);
468    }
469
470    #[test]
471    fn test_prefix_to_v4_mask_overflow() {
472        // prefix > 32 should saturate to all ones
473        assert_eq!(FilterBuilder::prefix_to_v4_mask(33), 0xFFFFFFFF);
474        assert_eq!(FilterBuilder::prefix_to_v4_mask(255), 0xFFFFFFFF);
475    }
476
477    #[test]
478    #[ignore] // Requires admin privileges
479    fn test_add_simple_filter() {
480        let engine = WfpEngine::new().expect("Failed to create engine");
481        crate::initialize_wfp(&engine).expect("Failed to initialize WFP");
482
483        let rule = FilterRule::allow_all_outbound();
484        let result = FilterBuilder::add_filter(&engine, &rule);
485
486        assert!(result.is_ok(), "Failed to add filter: {:?}", result);
487    }
488
489    #[test]
490    #[ignore] // Requires admin privileges
491    fn test_add_and_delete_filter() {
492        let engine = WfpEngine::new().expect("Failed to create engine");
493        crate::initialize_wfp(&engine).expect("Failed to initialize WFP");
494
495        let rule = FilterRule::allow_all_outbound();
496
497        let filter_id = FilterBuilder::add_filter(&engine, &rule).expect("Failed to add filter");
498        let result = FilterBuilder::delete_filter(&engine, filter_id);
499
500        assert!(result.is_ok(), "Failed to delete filter: {:?}", result);
501    }
502}