Skip to main content

windows_wfp/
filter_enum.rs

1//! WFP Filter Enumeration
2//!
3//! Enumerate active WFP filters in the system. Useful for debugging,
4//! auditing, and discovering filters from other firewall software.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use windows_wfp::{WfpEngine, FilterEnumerator, FilterInfo};
10//!
11//! let engine = WfpEngine::new()?;
12//! let filters: Vec<FilterInfo> = FilterEnumerator::all(&engine)?;
13//!
14//! for filter in &filters {
15//!     println!("{}: {} ({:?})", filter.id, filter.name, filter.action);
16//! }
17//! # Ok::<(), windows_wfp::WfpError>(())
18//! ```
19
20use crate::engine::WfpEngine;
21use crate::errors::{WfpError, WfpResult};
22use std::path::PathBuf;
23use std::ptr;
24use windows::core::GUID;
25use windows::Win32::Foundation::{ERROR_SUCCESS, HANDLE};
26use windows::Win32::NetworkManagement::WindowsFilteringPlatform::{
27    FwpmFilterCreateEnumHandle0, FwpmFilterDestroyEnumHandle0, FwpmFilterEnum0, FwpmFreeMemory0,
28    FWPM_FILTER0, FWP_ACTION_BLOCK, FWP_ACTION_CALLOUT_INSPECTION, FWP_ACTION_CALLOUT_TERMINATING,
29    FWP_ACTION_CALLOUT_UNKNOWN, FWP_ACTION_PERMIT, FWP_BYTE_BLOB_TYPE, FWP_EMPTY, FWP_UINT16,
30    FWP_UINT32, FWP_UINT64, FWP_UINT8,
31};
32
33use crate::constants::CONDITION_ALE_APP_ID;
34
35/// Action type of a WFP filter
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum FilterAction {
38    /// Block traffic
39    Block,
40    /// Permit traffic
41    Permit,
42    /// Callout (terminating)
43    CalloutTerminating,
44    /// Callout (inspection only)
45    CalloutInspection,
46    /// Callout (unknown)
47    CalloutUnknown,
48    /// Other action type
49    Other(u32),
50}
51
52/// Information about an active WFP filter
53#[derive(Debug, Clone)]
54pub struct FilterInfo {
55    /// WFP filter ID
56    pub id: u64,
57    /// Display name
58    pub name: String,
59    /// Description
60    pub description: String,
61    /// Filter action (Block, Permit, Callout)
62    pub action: FilterAction,
63    /// Provider GUID (if set)
64    pub provider_key: Option<GUID>,
65    /// Filter weight/priority
66    pub weight: u64,
67    /// Layer GUID where the filter is installed
68    pub layer_key: GUID,
69    /// Sublayer GUID
70    pub sublayer_key: GUID,
71    /// Application path extracted from conditions (if present)
72    pub app_path: Option<PathBuf>,
73    /// Number of conditions on this filter
74    pub num_conditions: u32,
75}
76
77/// Enumerates WFP filters from the system
78///
79/// Provides methods to list all active filters; callers can then filter by provider if desired.
80///
81/// # Example
82///
83/// ```no_run
84/// use windows_wfp::{WfpEngine, FilterEnumerator};
85///
86/// let engine = WfpEngine::new()?;
87///
88/// // List all filters
89/// let all = FilterEnumerator::all(&engine)?;
90/// println!("Total filters: {}", all.len());
91///
92/// // Filter by provider GUID
93/// use windows::core::GUID;
94/// let my_guid = GUID::from_u128(0x12345678_1234_5678_1234_567812345678);
95/// let mine: Vec<_> = all.iter().filter(|f| f.provider_key == Some(my_guid)).collect();
96/// # Ok::<(), windows_wfp::WfpError>(())
97/// ```
98pub struct FilterEnumerator;
99
100impl FilterEnumerator {
101    /// Enumerate all active WFP filters
102    ///
103    /// Returns a vector of [`FilterInfo`] for every filter currently registered in WFP.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the enumeration handle cannot be created or enumeration fails.
108    /// Requires administrator privileges.
109    pub fn all(engine: &WfpEngine) -> WfpResult<Vec<FilterInfo>> {
110        Self::enumerate_raw(engine, |filter_array, num_returned, acc: &mut Vec<FilterInfo>| {
111            for i in 0..num_returned {
112                unsafe {
113                    let filter = &**filter_array.offset(i as isize);
114                    acc.push(parse_filter(filter));
115                }
116            }
117        })
118    }
119
120    /// Count all active WFP filters without collecting details
121    ///
122    /// Counts filters without parsing their display names or conditions.
123    /// Requires administrator privileges.
124    pub fn count(engine: &WfpEngine) -> WfpResult<usize> {
125        Self::enumerate_raw(engine, |_filter_array, num_returned, acc: &mut usize| {
126            *acc += num_returned as usize;
127        })
128    }
129
130    /// Internal helper: create an enum handle, iterate batches, accumulate results, then destroy the handle.
131    ///
132    /// `visitor` is called for each batch with the raw filter pointer array, the number of
133    /// filters returned in that batch, and a mutable reference to the accumulator.
134    fn enumerate_raw<T, F>(engine: &WfpEngine, mut visitor: F) -> WfpResult<T>
135    where
136        T: Default,
137        F: FnMut(*mut *mut FWPM_FILTER0, u32, &mut T),
138    {
139        let mut enum_handle = HANDLE::default();
140
141        unsafe {
142            let result = FwpmFilterCreateEnumHandle0(engine.handle(), None, &mut enum_handle);
143            if result != ERROR_SUCCESS.0 {
144                return Err(WfpError::Other(format!(
145                    "Failed to create filter enum handle: error code {}",
146                    result
147                )));
148            }
149        }
150
151        let mut accumulator = T::default();
152        let batch_size = 100;
153
154        loop {
155            let mut filter_array: *mut *mut FWPM_FILTER0 = ptr::null_mut();
156            let mut num_returned: u32 = 0;
157
158            let result = unsafe {
159                FwpmFilterEnum0(
160                    engine.handle(),
161                    enum_handle,
162                    batch_size,
163                    &mut filter_array,
164                    &mut num_returned,
165                )
166            };
167
168            if result != ERROR_SUCCESS.0 {
169                unsafe {
170                    let _ = FwpmFilterDestroyEnumHandle0(engine.handle(), enum_handle);
171                }
172                return Err(WfpError::Other(format!(
173                    "Failed to enumerate filters: error code {}",
174                    result
175                )));
176            }
177
178            if num_returned == 0 {
179                unsafe {
180                    if !filter_array.is_null() {
181                        FwpmFreeMemory0(&mut filter_array as *mut _ as *mut *mut _);
182                    }
183                }
184                break;
185            }
186
187            visitor(filter_array, num_returned, &mut accumulator);
188
189            unsafe {
190                if !filter_array.is_null() {
191                    FwpmFreeMemory0(&mut filter_array as *mut _ as *mut *mut _);
192                }
193            }
194        }
195
196        unsafe {
197            let _ = FwpmFilterDestroyEnumHandle0(engine.handle(), enum_handle);
198        }
199
200        Ok(accumulator)
201    }
202}
203
204/// Parse a raw FWPM_FILTER0 into FilterInfo
205///
206/// # Safety
207///
208/// The filter pointer must be valid.
209unsafe fn parse_filter(filter: &FWPM_FILTER0) -> FilterInfo {
210    let name = if !filter.displayData.name.is_null() {
211        filter
212            .displayData
213            .name
214            .to_string()
215            .unwrap_or_else(|_| String::new())
216    } else {
217        String::new()
218    };
219
220    let description = if !filter.displayData.description.is_null() {
221        filter
222            .displayData
223            .description
224            .to_string()
225            .unwrap_or_else(|_| String::new())
226    } else {
227        String::new()
228    };
229
230    let action = match filter.action.r#type {
231        FWP_ACTION_BLOCK => FilterAction::Block,
232        FWP_ACTION_PERMIT => FilterAction::Permit,
233        FWP_ACTION_CALLOUT_TERMINATING => FilterAction::CalloutTerminating,
234        FWP_ACTION_CALLOUT_INSPECTION => FilterAction::CalloutInspection,
235        FWP_ACTION_CALLOUT_UNKNOWN => FilterAction::CalloutUnknown,
236        other => FilterAction::Other(other.0),
237    };
238
239    let provider_key = if !filter.providerKey.is_null() {
240        Some(*filter.providerKey)
241    } else {
242        None
243    };
244
245    let weight = match filter.weight.r#type {
246        FWP_UINT8 => filter.weight.Anonymous.uint8 as u64,
247        FWP_UINT16 => filter.weight.Anonymous.uint16 as u64,
248        FWP_UINT32 => filter.weight.Anonymous.uint32 as u64,
249        FWP_UINT64 => {
250            let ptr = filter.weight.Anonymous.uint64;
251            if ptr.is_null() {
252                0
253            } else {
254                *ptr
255            }
256        }
257        FWP_EMPTY => 0,
258        _ => 0,
259    };
260
261    let app_path = extract_app_path(filter);
262
263    FilterInfo {
264        id: filter.filterId,
265        name,
266        description,
267        action,
268        provider_key,
269        weight,
270        layer_key: filter.layerKey,
271        sublayer_key: filter.subLayerKey,
272        app_path,
273        num_conditions: filter.numFilterConditions,
274    }
275}
276
277/// Extract application path from filter conditions
278///
279/// Looks for FWPM_CONDITION_ALE_APP_ID in the filter's conditions
280/// and decodes the wide-string blob.
281unsafe fn extract_app_path(filter: &FWPM_FILTER0) -> Option<PathBuf> {
282    if filter.numFilterConditions == 0 || filter.filterCondition.is_null() {
283        return None;
284    }
285
286    let conditions =
287        std::slice::from_raw_parts(filter.filterCondition, filter.numFilterConditions as usize);
288
289    let condition = conditions
290        .iter()
291        .find(|c| c.fieldKey == CONDITION_ALE_APP_ID)?;
292
293    if condition.conditionValue.r#type != FWP_BYTE_BLOB_TYPE {
294        return None;
295    }
296
297    let blob_ptr = condition.conditionValue.Anonymous.byteBlob;
298    if blob_ptr.is_null() {
299        return None;
300    }
301
302    let blob = &*blob_ptr;
303    if blob.data.is_null() || blob.size == 0 || (blob.size % 2) != 0 {
304        return None;
305    }
306
307    let wide_slice = std::slice::from_raw_parts(blob.data as *const u16, (blob.size / 2) as usize);
308    let null_pos = wide_slice
309        .iter()
310        .position(|&c| c == 0)
311        .unwrap_or(wide_slice.len());
312
313    String::from_utf16(&wide_slice[..null_pos])
314        .ok()
315        .map(PathBuf::from)
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_filter_action_eq() {
324        assert_eq!(FilterAction::Block, FilterAction::Block);
325        assert_eq!(FilterAction::Permit, FilterAction::Permit);
326        assert_ne!(FilterAction::Block, FilterAction::Permit);
327        assert_eq!(FilterAction::Other(42), FilterAction::Other(42));
328        assert_ne!(FilterAction::Other(1), FilterAction::Other(2));
329    }
330
331    #[test]
332    fn test_filter_action_copy() {
333        let action = FilterAction::Block;
334        let copy = action;
335        assert_eq!(action, copy);
336    }
337
338    #[test]
339    fn test_filter_info_clone() {
340        let info = FilterInfo {
341            id: 42,
342            name: "Test filter".to_string(),
343            description: "A test".to_string(),
344            action: FilterAction::Block,
345            provider_key: None,
346            weight: 1000,
347            layer_key: GUID::zeroed(),
348            sublayer_key: GUID::zeroed(),
349            app_path: Some(PathBuf::from(r"C:\test.exe")),
350            num_conditions: 1,
351        };
352
353        let cloned = info.clone();
354        assert_eq!(cloned.id, 42);
355        assert_eq!(cloned.name, "Test filter");
356        assert_eq!(cloned.action, FilterAction::Block);
357        assert!(cloned.app_path.is_some());
358    }
359
360    #[test]
361    fn test_filter_info_default_values() {
362        let info = FilterInfo {
363            id: 0,
364            name: String::new(),
365            description: String::new(),
366            action: FilterAction::Permit,
367            provider_key: None,
368            weight: 0,
369            layer_key: GUID::zeroed(),
370            sublayer_key: GUID::zeroed(),
371            app_path: None,
372            num_conditions: 0,
373        };
374
375        assert_eq!(info.id, 0);
376        assert!(info.name.is_empty());
377        assert!(info.provider_key.is_none());
378        assert!(info.app_path.is_none());
379    }
380
381    #[test]
382    fn test_filter_info_with_provider() {
383        let guid = GUID::from_u128(0x12345678_1234_5678_1234_567812345678);
384        let info = FilterInfo {
385            id: 100,
386            name: "Provider filter".to_string(),
387            description: String::new(),
388            action: FilterAction::Block,
389            provider_key: Some(guid),
390            weight: 5000,
391            layer_key: GUID::zeroed(),
392            sublayer_key: GUID::zeroed(),
393            app_path: None,
394            num_conditions: 3,
395        };
396
397        assert_eq!(info.provider_key, Some(guid));
398        assert_eq!(info.num_conditions, 3);
399    }
400
401    #[test]
402    #[ignore] // Requires administrator privileges
403    fn test_enumerate_all_filters() {
404        let engine = WfpEngine::new().expect("Failed to open WFP engine");
405        let filters = FilterEnumerator::all(&engine).expect("Failed to enumerate");
406        // Any Windows system should have at least some WFP filters
407        assert!(!filters.is_empty(), "Expected at least one WFP filter");
408    }
409
410    #[test]
411    #[ignore] // Requires administrator privileges
412    fn test_count_filters() {
413        let engine = WfpEngine::new().expect("Failed to open WFP engine");
414        let count = FilterEnumerator::count(&engine).expect("Failed to count");
415        let all = FilterEnumerator::all(&engine).expect("Failed to enumerate");
416        assert_eq!(count, all.len());
417    }
418}