尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++ WFP个人防火墙开发实战:从过滤引擎到规则管理

C++ WFP个人防火墙开发实战:从过滤引擎到规则管理 简介这是面向C开发者和网络安全初学者的Windows个人防火墙实现基于WFPWindows Filter Platform框架完成网络流量过滤、出入站规则管理等核心功能适合用来学习Windows网络驱动编程与防火墙设备原理。压缩包共50个文件体积仅229KB包含16个头文件和13个C/C源文件9个cpp、4个c对应WFP会话搭建、过滤层注册、流量判断与规则管理等关键逻辑同时附有Visual Studio工程配置sln/vcxproj、WFP筛选器定义filters、安装配置说明inf以及界面资源rc/ico/png。该资源在CSDN已有104人次学习下载具备一定参考价值。通过源码可直观看到个人防火墙常用模块的实际组织方式包括规则引擎、流量日志、界面交互等并能对照学习完整项目的目录结构与编码思路。适合希望从零搭建网络防护工具或深入研究WFP API的读者参考。1. 用C写个人防火墙先从WFP的边界谈起用C写个人防火墙路线比很多人想象的少要么做NDIS中间层驱动要么做TDI过滤要么用WFP。前两条路线在Vista之后就不被官方推荐了工作位置太靠近协议栈底层连接感知、状态跟踪全要自己实现一个驱动挂掉可能蓝屏整个系统。WFPWindows Filter Platform把协议栈切分成数十个过滤层由系统引擎统一调度过滤器个人防火墙这类需求会变成填规则表的事。这份基于C的WFPFirewall个人防火墙系统正好是C八股和网络编程书里难得见到完整源码的内核实战样本从驱动初始化、Callout拦截到用户态规则管理是整条链路适合想搞懂Windows网络过滤、或准备C面试八股时想讲出内核深度的读者。后文按架构、驱动、规则管理、调试的顺序拆开讲。2. 过滤引擎与分层模型先把WFP的架构踩实2.1 从过滤点到分层的架构演进过去做NDIS过滤驱动时最难处理的不是包怎么拦而是在哪一层拦太底层看不到TCP端口太上层又要自己维护连接表。WFP把网络操作抽象成两类路径数据流动线收发、转发和连接建立时刻bind、connect、accept。这两类对应不同的FWPM_LAYER_*过滤层。个人防火墙最常见的需求里禁止某个进程访问外网要找ALE_AUTH_CONNECT层禁止外部机器连入本机端口要找ALE_AUTH_RECV_ACCEPT层。WFP在每个层上做的是同一件事把当前包的特征和过滤器的条件做匹配命中就执行过滤器的动作PERMIT或BLOCK。这一设计把位置选择和规则判断解耦收益在写代码时就能感受到不用自己解析IP头找端口WFP已经把五元组、进程ID、AppId这些字段全部填充好了。这也是为什么替代NDIS过滤驱动时迁移成本集中在重新理解层模型而不是重写包解析逻辑。2.2 两类句柄引擎句柄与会话语义使用WFP API时第一个动作是获得引擎句柄。FwpmEngineOpen0打开的是筛选引擎Filter Engine的会话后续所有规则操作都挂在这个会话上。这里有个关键区别以FWPM_SESSION_FLAG_DYNAMIC标志打开的会话句柄关闭时自动清理其中注册的所有过滤器不带这个标志过滤器默认持续有效直到系统重启。持久化规则必须用非动态会话提交临时测试规则则建议挂到动态会话上避免调试时反复重启系统。一个项目里往往不止一套句柄。除了规则管理用的引擎句柄还有内核态Callout注册返回的句柄以及用户态和驱动通信用的设备对象句柄。管理侧通过FWPM引擎API提交规则数据路径通过Callout执行自定义逻辑两条线在FWPM_FILTER0引用Callout时交汇。// 引擎会话方式决定过滤器生命周期这是防火墙规则是否持久化的前提 FWPM_SESSION session { 0 }; session.flags 0; // 0 表示持久会话FWPM_SESSION_FLAG_DYNAMIC 表示临时会话 session.txnWaitTimeoutInMSec 2000; // 事务等待上限防止RPC调用长期阻塞 HANDLE engineHandle NULL; DWORD wtError FwpmEngineOpen0( NULL, // 目标机器名NULL 代表本机 RPC_C_AUTHN_WINNT, // 本机调用保持默认认证即可 NULL, // 访问控制NULL 表示默认安全描述符 session, engineHandle ); if (wtError ! ERROR_SUCCESS) { // 常见失败原因未以管理员权限运行或与WFP基础服务通信超时 return wtError; }打开引擎之后的几乎所有操作包括添加、枚举、删除过滤器都要复用这个句柄。session里的txnWaitTimeoutInMSec并非越小越好因为导入百条规则时单次事务可能超过2000毫秒调太短会导致大型规则集提交失败。2.3 过滤器条件、权重与动作三元组任何一条实际生效的规则在WFP里都被描述成一个FWPM_FILTER0结构体最核心的字段是filterCondition、weight、action。条件数组中的每一项由字段标识符例如FWPM_CONDITION_IP_LOCAL_PORT、匹配方式FWP_MATCH_EQUAL和值FWP_UINT16等组成。权重是分层模型里最反直觉的部分。同一过滤层上存在多个过滤器时引擎严格按照权重大小依次校验权重大的先匹配若命中BLOCK就直接裁决权重小的过滤器根本没有机会执行。所以个人防火墙里的阻止规则要显式给高权重比如0xFF000000以上放行规则放低权重区间。很多初学者按添加顺序理解规则但WFP不保证规则顺序只保证按权重排序。action字段的可选值除了PERMIT和BLOCK还有FWP_ACTION_CALLOUT_TERMINATING和FWP_ACTION_CALLOUT_INSPECTION。前者在过滤动作执行完毕后终止判断由Callout返回最终裁决后者只是观测继续给下层过滤器判断。做日志审计时会用INSPECTION做强制拦截用TERMINATING或直接BLOCK。2.4 核心过滤层与个人防火墙场景对照实际操作前先把个人防火墙常用的几个层对应起来。项目大量使用V4和V6成对层如果只注册V4层IPv6流量就跟裸奔一样没有任何拦截这是此类项目里最常见的漏洞。过滤层标识触发时机个人防火墙用途FWPM_LAYER_ALE_AUTH_CONNECT_V4/V6应用发起出站TCP连接或UDP发送前禁止应用外联、按目标IP做阻断FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4/V6入站连接完成握手或UDP接收前端口防护、只允许白名单IP访问FWPM_LAYER_ALE_RESOURCE_ASSIGN_V4/V6bind()绑定端口时限制指定进程只能监听指定端口FWPM_LAYER_OUTBOUND_TRANSPORT_V4/V6每次出站包过传输层时按IP/端口做细粒度包过滤FWPM_LAYER_IPFORWARD_V4/V6IPv4/IPv6转发时主机侧轻量转发控制选层原则是越接近连接建立越好。在ALE层拦截时系统还没有真正发起或接受连接能拿到进程ID和路径且开销最小。OUTBOUND_TRANSPORT层是每个出站包都会进来一次适合做包内容或频率判断纯端口规则放这里属于性能浪费。这个判断在调试时很重要如果规则加到了OUTBOUND_TRANSPORT但期望的是连接级拦截可能看到同一个连接的首个包被处理了多次。3. 从Callout驱动到规则注入把拦截链路打通3.1 什么时候必须写Callout很多纯按五元组封禁的场景在用户态直接调用FwpmFilterAdd0就结束了不需要内核驱动。这个项目里出现Callout主要目的不是做包过滤本身而是做两件用户态过滤表达式做不了的事一是拿用户态API无法获取的独立上下文比如统计每进程连接数、做动态端口白名单联动二是在FWP_ACTION_REDIRECT模式下做流量转向。理解这一点就不会把这套代码理解成一个必须照抄的驱动样板而是当成规则引擎的扩展钩子。3.2 驱动入口和Callout注册顺序写出WFP可识别的Callout至少需要实现两个回调classifyFn在每次包经过绑定的过滤层时被调用notifyFn在过滤器添加或删除时收到通知。驱动入口里通常先创建设备对象和符号链接再调用FwpsCalloutRegister0注册FWPS_CALLOUT0结构体。// WFP 驱动入口的典型骨架实际项目中对照驱动工程的 DriverEntry 查看 extern C NTSTATUS DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) { // 1. 创建设备用于后续向用户态回传日志和计数 UNICODE_STRING deviceName RTL_CONSTANT_STRING(L\\Device\\WfpFwDevice); UNICODE_STRING symLinkName RTL_CONSTANT_STRING(L\\DosDevices\\WfpFwDevice); IoCreateDevice(driverObject, 0, deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, g_deviceObject); IoCreateSymbolicLink(symLinkName, deviceName); // 2. 注册 WFP Callout FWPS_CALLOUT0 callout { 0 }; callout.flags 0; callout.classifyFn WfpFwClassify; // 主分类回调每条规则命中时进入 callout.notifyFn WfpFwNotify; // 通知回调规则增删时进入 callout.flowDeleteFn NULL; // 不跟踪流按需扩展 UINT32 calloutId 0; NTSTATUS status FwpsCalloutRegister0(g_deviceObject, callout, calloutId); if (!NT_SUCCESS(status)) { return status; } g_calloutId calloutId; // 3. 把 calloutId 暴露给用户态用户态用 FwpmCalloutAdd0 提交后方可被过滤器引用 return STATUS_SUCCESS; }calloutId是系统为这个Callout分配的唯一标识后续用户态通过FwpmCalloutAdd0提交同一标识的Callout信息两边才能对应上。classifyFn是主回调每次有包经过绑定的过滤层都会进入notifyFn用于在过滤器生命周期变化时清理驱动侧资源。deviceObject是必备的即使功能上不主动向用户态发消息也要让用户态能打开设备句柄做IOCTL通信否则规则管理和驱动状态就完全割裂了。3.3 classifyFn 里怎么写分类逻辑classifyFn的inFixedValues包含当前包的数据字段比如IP、端口、协议和所在过滤层inMetaValues包含进程ID、应用路径等元数据。驱动侧逻辑不要重复用户态规则而是做用户态规则够不到的部分。以项目场景为例判断进程可执行文件路径是否在放行名单里就是典型的Callout工作。// classifyFn 回调顺序执行每一个过滤器给出的判断结果 VOID WfpFwClassify(const FWPS_INCOMING_VALUES* inFixedValues, const FWPS_INCOMING_METADATA_VALUES* inMetaValues, void* layerData, const void* classifyContext, FWPS_FILTER* filter, UINT64 flowContext, FWPS_CLASSIFY_OUT* classifyOut) { // 默认动作按过滤器预设值执行通常来自用户态 FwpmFilterAdd0 的 action 字段 classifyOut-actionType FWP_ACTION_PERMIT; // 在元数据里拿到进程路径命中黑名单则直接拦截 if (inMetaValues-currentMetadata FWPS_METADATA_FIELD_PROCESS_PATH) { UNICODE_STRING* processPath NULL; FwpsQueryProcessInformation(inMetaValues-processId, FWPS_FIELD_ALE_AUTH_CONNECT_V4_ALE_APP_ID, NULL, 0, (void**)processPath); if (wcsstr(processPath-Buffer, L\\badapp.exe) ! NULL) { classifyOut-actionType FWP_ACTION_BLOCK; classifyOut-rights 0; // 清零表示不再丢给下层过滤器继续判断 } } }每个过滤器进入classifyFn是因为它在当前过滤层的权重和条件都命中了。如果这里什么都不做默认的FWP_ACTION_PERMIT会把包放走所以必须对策略明确的场景才返回BLOCK。classifyOut-rights清零是容易忽略的点不清零时即使本过滤器返回BLOCK后续过滤器仍有机会叠加判断产生上一个拦截、下一个放行的冲突结果。审计类Callout则应该保留rights只记录不拦截。3.4 用户态把规则注入到引擎用户态规则注入是整个项目里最常被复制粘贴的部分步骤只有三个打开引擎、开事务、提交过滤器。WFP的API支持批量事务一次事务可以提交多条规则要么全部生效要么全部回滚个人防火墙导入规则包时非常依赖这个能力。// 用户态将一个阻止 80 端口入站访问的规则提交到 ALE RECV/ACCEPT 层 FWPM_FILTER0 filter { 0 }; filter.layerKey FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; // 入站连接层 filter.displayData.name LBlock Inbound 80; filter.action.type FWP_ACTION_BLOCK; filter.weight.type FWP_UINT64; filter.weight.uint64 0xFF000001; // 显式高权重防止被放行规则覆盖 // 条件数组一条是本地端口 80 FWPM_FILTER_CONDITION0 conds[1] { 0 }; conds[0].fieldKey FWPM_CONDITION_IP_LOCAL_PORT; conds[0].matchType FWP_MATCH_EQUAL; UINT16 port 80; conds[0].conditionValue.type FWP_UINT16; conds[0].conditionValue.uint16 port; filter.filterCondition conds; filter.numFilterConditions 1; UINT64 filterId 0; DWORD result FwpmFilterAdd0(engineHandle, filter, NULL, filterId);这里有个容易踩的坑在ALE层拦截入站80端口只用IP_LOCAL_PORT条件可行因为系统会为监听端口自动匹配但如果是特定远程IP禁止连接本机必须同时加FWPM_CONDITION_REMOTE_ADDRESS否则过滤器会对所有来源IP生效直接打穿白名单逻辑。filterWeight如果不设置引擎会给默认值多条默认权重规则相互覆盖时顺序不定所以生产环境一定显式赋值。4. 规则数据结构与动态更新像产品一样管规则4.1 规则模型从五元组到策略FWPM_FILTER0只是一个通用容器真实产品的规则需要自己的抽象层。这个项目的规则模型适合按策略组织而不是直接暴露filterCondition数组给界面。一条完整策略由方向出站/入站/转发、协议TCP/UDP/ICMP、本地端口区间、远程地址段、目标进程路径、动作、生效时段构成。编译成FWPM_FILTER0的condition数组时每种维度对应一个或多个条件项。规则字段对应FWPM条件类型说明本地端口FWPM_CONDITION_IP_LOCAL_PORTFWP_UINT16配合RANGE匹配表示端口区间远程IPFWPM_CONDITION_IP_REMOTE_ADDRESSFWP_V4_ADDR_MASKV4地址带掩码V6用FWP_BYTE_ARRAY16_TYPE协议FWPM_CONDITION_IP_PROTOCOLFWP_UINT86TCP17UDP1ICMP进程路径FWPM_CONDITION_ALE_APP_IDFWP_BYTE_BLOB需封装成FWP_APP_ID格式的SDDL路径协议和端口字段用FWP_MATCH_RANGE可以覆盖1-1024端口这类连续区间不必拆成多条规则。IP地址的类型是另一个坑V4层有时看到代码里用FWP_BYTE_ARRAY16_TYPE填地址那实际是IPv4-mapped IPv6格式初看容易跟V6地址混淆。如果规则匹配始终不生效先检查条件值的类型是否与所在过滤层的地址类型完全一致。4.2 用事务把批量规则原子化个人防火墙首次安装需要恢复规则集一次可能加几百条过滤器。逐条调用FwpmFilterAdd0在失败时会产生半套规则集还会因为反复RPC往返导致安装卡顿。正确做法是把整批提交放进事务。// 事务边界内的规则要么全部生效要么全部回滚 DWORD error FwpmTransactionBegin0(engineHandle, 0); if (error ! ERROR_SUCCESS) { return error; // 事务不支持嵌套重复 begin 会返回错误 } for (size_t i 0; i rules.size(); i) { error FwpmFilterAdd0(engineHandle, rules[i].filter, NULL, rules[i].filterId); if (error ! ERROR_SUCCESS) { FwpmTransactionAbort0(engineHandle); // 任一条失败就撤销前面所有规则 return error; } } error FwpmTransactionCommit0(engineHandle);事务的flags通常传0。事务的作用域随引擎句柄的会话同一会话内不同线程的操作也会被纳入同一个事务所以并发写入时需要在线程外部加锁。FwpmTransactionAbort0会回滚整个会话里未提交的所有操作如果同时有其他逻辑在向引擎写数据要避免abort时误伤无关操作一般把事务边界收得越小越好。4.3 动态更新不动驱动改规则防火墙运行时最常遇到的操作是临时放行某个IP十分钟或者马上封禁一个IP。在WFP里做动态更新仍然走用户态API不需要重启驱动也不需要动Callout。核心操作是枚举过滤器和删除指定ID。// 按条件枚举出所有与 TempAllow 相关的过滤器 FWPM_FILTER_ENUM_TEMPLATE0 enumTemplate { 0 }; enumTemplate.layerKey FWPM_LAYER_ALE_AUTH_CONNECT_V4; enumTemplate.actionMask FWP_ACTION_BLOCK | FWP_ACTION_PERMIT; HANDLE enumHandle NULL; FwpmFilterCreateEnumHandle0(engineHandle, enumTemplate, enumHandle); FWPM_FILTER0** entries NULL; DWORD count 0; FwpmFilterEnum0(engineHandle, enumHandle, 100, entries, count); for (DWORD i 0; i count; i) { if (wcsstr(entries[i]-displayData.name, LTempAllow) ! NULL) { FwpmFilterDeleteById0(engineHandle, entries[i]-filterId); } } FwpmFilterDestroyEnumHandle0(engineHandle, enumHandle);枚举模板里的actionMask如果只想找BLOCK规则就写FWP_ACTION_BLOCK写成PERMIT会匹配不上BLOCK类型的过滤器。动态规则不建议专门写驱动去维护时间戳直接在用户态做先删后加的逻辑时长到了再删一次出问题时还能用netsh看到真实列表。4.4 持久化和开机自启规则持久化往往会退化成读写配置文件真正要处理的是配置文件与引擎状态不一致。推荐策略是把规则文件设计成唯一的事实来源引擎里的过滤器只是它的运行时投影。开机后防火墙程序先读规则文件再按4.2的事务整体重建全部过滤器。这样引擎崩溃、规则被第三方误删、系统升级后过滤器丢失下一次重启都会自动恢复。服务启动脚本里要等待WinFW服务完全就绪再执行注入否则FwpmEngineOpen0可能返回RPC_S_SERVER_UNAVAILABLE被误判成驱动加载失败。规则文件格式建议带版本号和校验字段项目里序列化模块通常预留版本字段就是为了将来规则格式升级时还能向后兼容。5. 调试验证与扩展netsh转储驱动的每一笔决策5.1 用netsh把过滤器拉到明面上开发WFP接近尾声时我很少在代码里printf而是开一个管理员终端执行netsh wfp show filters。这个命令会把系统里所有过滤层上的过滤器按权重排序打印出来包括Windows自带的和你自己框架注册的。定位问题的第一步永远是看两个点过滤器是否真的加进来了它在过滤器列表里的优先级对不对。如果规则加了但网络行为没变化第2章和第3章里的层选错是最常见的原因。net session nul 21 || (echo 需要管理员权限 exit /b 1) netsh wfp show filters wfp_filters_%date:~0,4%%date:~5,2%%date:~8,2%.txt netsh wfp show state filewfp_state.xml第一行在批处理里确认管理员权限后两行分别导出过滤器列表和完整状态文件。wfp_state.xml里包含所有过滤器的条件、动作以及引用的Callout ID拿它和代码里注册的顺序对照能很快发现规则存在但根本没被匹配的灵异事件。5.2 三个高频故障与排查对照个人防火墙联调阶段会反复遇到几类问题。一是IPv6被遗忘只在V4层注册V6流量完全裸奔查看show filters结果里V6层过滤器数量是否为零。二是条件字段与数据流方向不对称UDP数据发送不经过RECV_ACCEPT层需要RESOURCE_ASSIGN和RECV_ACCEPT配合使用。三是默认放行陷阱任何未匹配到的包最终会被系统默认放行必须为关键端口显式写BLOCK而不是依赖规则顺序。驱动内部的classifyFn执行路径可以用WPP日志或ETW Provider标记进入点在WDK的Device Verifier里打开对应IO追踪项日志里就能看到每次进出的调用栈。5.3 不改动驱动扩展成流量审计工具规则更新完全不用触碰驱动这让WFP在扩展产品形态时很占便宜。在第3章的Callout上挂一个FWP_ACTION_CALLOUT_INSPECTION类型的过滤器让Callout在首包事件里通过ETW写出进程ID与端口用户态采集服务消费ETW即可得到全进程出站记录不用去插NDIS钩子写环形缓冲区。另一个实用扩展是变更前的规则快照每次事务提交前导出过滤器ID集合误拦漏放时和当前集合做差集配合netsh的状态转储能把规则差分定位的时间从半天压到几分钟。本文还有配套的精品资源点击获取
返回列表