
简介一套Windows平台下的包过滤防火墙完整源码面向网络安全方向学生、C开发者及对防火墙底层实现有兴趣的读者。项目以MFC构建管理界面并配合驱动过滤程序完整演示从数据包捕获、规则匹配到策略配置的基本链路。资源包共34个文件、约89KB其中.h/.cpp是核心逻辑源码.dsp/.dsw为VC工程配置.rc/.rc2描述对话框、菜单等界面资源另有.sys驱动镜像和.exe演示程序可直接运行文件体系从源码、工程脚本到二进制输出覆盖完整便于对照阅读、编译和二次修改。重点模块涉及规则对话框、驱动通信与Socket封装能帮助读者梳理驱动层拦截、规则库组织与上层管理之间的协作关系并从中学习如何设计简单规则引擎、如何加载驱动以及如何维护规则列表等实现细节。目前已有757人学习下载对进行防火墙课设或准备投入网络安全开发的读者而言是一份轻量清晰且可直接参考的完整工程。1. 一个能编译运行的Windows防火墙样本这个源码包的价值不在成品功能多强而在于它把一台 Windows 主机上的防火墙拆成了可以逐行阅读的完整链路MFC 对话框负责规则配置TDriver 负责与驱动通信DrvFltIp.sys 在内核态完成真正的 IP 包过滤两端通过 IOCTL 完成规则下发和状态同步。包里有现成的 Firewall.exe 可执行程序、DrvFltIp.sys 驱动文件以及 RuleDlg.cpp、rules.h、sockUtil.cpp 等关键模块源码适合安全方向的开发者和想做网络防护的运维人员学习包过滤在 Windows 上到底发生在哪一层。读完能回答两个问题规则是怎么从界面一路到达内核的以及一个 IP 包在内核里经历了哪些判断才被放行或丢弃。2. 源码结构拆解界面层、驱动层与工具模块怎么分工2.1 从 FirewallApp.dsw 看工程构成这个工程是典型的 VC6 时代 MFC Driver 双工程结构FirewallApp.dsw 是工作区文件里面同时管理着应用层工程和驱动工程。先从文件清单里把模块边界划出来方便后面定位关键代码。分组文件职责MFC 框架FirewallApp.h/cpp、MainFrm.h/cpp、FirewallAppView.h/cpp、FirewallAppDoc.h/cpp程序入口、主窗口、文档视图结构规则配置界面RuleDlg.h/cpp包过滤规则的增删改查对话框驱动通信封装TDriver.h/cpp驱动的加载、卸载、IOCTL 通信过滤驱动DrvFltIp.h、DrvFltIp.sys内核态 IP 包过滤主体及编译产物Socket 工具sockUtil.h/cpp本地地址获取、端口检测等辅助函数资源与版本FirewallApp.rc、.ico、Toolbar.bmp、resource.h菜单、图标、工具栏注意 CVS 开头的文件夹是早期版本控制遗留Entries、Root、Repository 这些文件在阅读时可以直接忽略。StdAfx.h是预编译头配置DrvFltIp.sys和Firewall.exe是已经编出的产物说明这套代码在原来的开发环境里是能完整构建的。2.2 驱动层DrvFltIp 与 TDriver 的分工DrvFltIp 是全工程的核心它的命名已经暗示了实现思路对 IP 层流量做过滤。它的头文件在用户态和驱动态两头都被引用里面定义了规则结构体和 IOCTL 控制码这是两个模块能够通信的基础。TDriver 是应用层对驱动的封装类封装了CreateFile打开驱动设备、DeviceIoControl下发控制码、CloseHandle关闭设备这些琐碎操作让 RuleDlg 调用时不需要直接面对 Win32 设备读写细节。这个架构和现在基于 WFPWindows Filtering Platform的防火墙思路有本质区别。DrvFltIp 走的是早期 NDIS/TDI 钩挂方案在协议栈里拦截 IP 包好处是代码路径短、原理直观坏处是兼容性受操作系统版本影响大在较新 Windows 上加载会有签名和版本限制。对于学习包过滤来说反而是这种简单的挂钩方式更容易看清数据流WFP 的分层和注入点太多反而不适合做入门分析。2.3 界面与辅助代码的职责边界RuleDlg 是防火墙规则的配置入口界面字段应该覆盖协议类型、方向、地址范围、端口号、动作以及规则列表的增删改查。sockUtil 里放的是与规则无关的 socket 辅助函数比如获取本机 IP 列表、把字符串 IP 转成in_addr结构这些函数被 RuleDlg 调用用于把界面上输入的文本转换成规则结构体里的数值字段。MainFrm 负责加载工具栏、创建状态栏FirewallAppView 负责输出过滤日志或当前规则状态。从这个分层能看出作者把界面展示、规则配置、驱动通信、内核过滤拆成了四个独立模块修改规则匹配逻辑时只需要动 DrvFltIp不需要重绘界面这也是一个合格的防火墙工程该有的解耦方式。3. 包过滤规则引擎规则数据结构与匹配计算3.1 规则条目怎么存规则的实际形状是理解整个过滤逻辑的入口。rules.h 里定义的结构体常见做法是让用户态和内核态共用同一个布局这样下发规则时可以直接把内存块拷贝给驱动不需要在每个字段上做序列化。我一般倾向于在这种场景下用固定长度的字节对齐结构体。#define RULE_DIR_IN 0x01 #define RULE_DIR_OUT 0x02 #define RULE_DIR_ANY 0x03 #define RULE_PROTO_ANY 0x00 #define RULE_PROTO_TCP 0x06 #define RULE_PROTO_UDP 0x11 #define RULE_PROTO_ICMP 0x01 #define RULE_ACTION_DROP 0x00 #define RULE_ACTION_PASS 0x01 typedef struct _FIREWALL_RULE { UCHAR ucDirection; // 规则匹配方向见 RULE_DIR_* UCHAR ucProtocol; // 协议编号取 IPPROTO_* 或 RULE_PROTO_ANY UCHAR ucAction; // 命中后动作放行或丢弃 UCHAR ucReserved; ULONG ulLocalAddr; // 本地 IP0 表示不限制 ULONG ulRemoteAddr; // 远端 IP0 表示不限制 USHORT usLocalPort; // 本地端口0 表示不限制 USHORT usRemotePort; // 远端端口0 表示不限制 } FIREWALL_RULE, *PFIREWALL_RULE;字段设计的考虑协议号和 TCP/IP 协议栈的 IPPROTO_ 常量保持一致匹配时可以直接拿包的协议字段比较省一次映射。IP 地址用ULONG存储网络字节序比较前不需要转换字节序但显示到界面时要通过ntohl转成点分十进制。端口同理。3.2 匹配逻辑与参数说明驱动拿到一个包后先提取方向、协议、地址、端口字段再与规则数组做线性匹配。匹配顺序很关键我把这个需求拆成三步先比方向再比协议最后比地址和端口任意一步失败直接退回下一条规则。命中后再根据动作字段决定放行还是丢弃。BOOLEAN MatchPacket( PFIREWALL_RULE pRules, ULONG ulRuleCount, PNET_PACKET_INFO pPacket) { for (ULONG i 0; i ulRuleCount; i) { PFIREWALL_RULE rule pRules[i]; if (rule-ucDirection ! RULE_DIR_ANY rule-ucDirection ! pPacket-ucDirection) { continue; } if (rule-ucProtocol ! RULE_PROTO_ANY rule-ucProtocol ! pPacket-ucProtocol) { continue; } if (rule-ulLocalAddr ! 0 rule-ulLocalAddr ! pPacket-ulLocalAddr) { continue; } if (rule-ulRemoteAddr ! 0 rule-ulRemoteAddr ! pPacket-ulRemoteAddr) { continue; } if (rule-usLocalPort ! 0 rule-usLocalPort ! pPacket-usLocalPort) { continue; } if (rule-usRemotePort ! 0 rule-usRemotePort ! pPacket-usRemotePort) { continue; } return (rule-ucAction RULE_ACTION_PASS) ? TRUE : FALSE; } return TRUE; // 无匹配规则时默认放行 }这段逻辑里的关键参数是RULE_DIR_ANY和RULE_PROTO_ANY。把所有限制条件设计成「0 表示任意」而不是单独一个开关位能让规则数组统一为固定结构体下发时就是一个连续内存块。默认放行的策略适合作为学习样本但实际部署时要注意规则表未命中的流量全部通过等于防火墙只对已声明规则生效。3.3 规则加载与界面绑定RuleDlg 里维护的规则列表最终要转换成 FIREWALL_RULE 数组下发给驱动。界面层常见的做法是让每条规则对应一个CListCtrl行编辑完成后统一收集到CArrayFIREWALL_RULE里再调用 TDriver 的发送接口。void CRuleDlg::OnApplyRules() { CArrayFIREWALL_RULE, FIREWALL_RULE arrRules; INT_PTR nCount m_listRules.GetItemCount(); for (INT_PTR i 0; i nCount; i) { FIREWALL_RULE rule { 0 }; // 从界面控件解析协议、方向、端口和动作 ParseProtocol(rule.ucProtocol, m_listRules.GetItemText(i, 1)); ParsePort(rule.usLocalPort, m_listRules.GetItemText(i, 2)); m_arrRules.Add(rule); } m_driver.SetRules(m_arrRules.GetData(), (DWORD)m_arrRules.GetSize()); }这里把界面行索引与规则字段的对应关系定死后面驱动回调去匹配时对所有规则一视同仁。SetRules 内部会走 IOCTL把规则数组首地址和数据长度传给内核驱动在 IRP 处理函数里将这份数据拷贝到自己的内存池中再替换旧的规则表。 ## 4. 用户态与内核态通信IOCTL 下发与数据包处理链路 ### 4.1 TDriver 的 IOCTL 通道 TDriver 对驱动的封装方式决定了应用层代码的整洁度。设备打开成功后规则下发通过 DeviceIoControl 完成。在驱动端这个调用会到达 IRP_MJ_DEVICE_CONTROL 的派遣函数控制码决定了驱动把输入缓冲当规则集还是查询请求。 c BOOL TDriver::SetRules(PFIREWALL_RULE pRules, DWORD dwCount) { DWORD dwBytesReturned 0; BOOL bRet DeviceIoControl( m_hDevice, // 驱动设备句柄 IOCTL_FIREWALL_SET_RULES, // 自定义控制码 pRules, // 输入缓冲规则数组 dwCount * sizeof(FIREWALL_RULE), // 输入缓冲长度 NULL, // 无输出数据 0, dwBytesReturned, // 驱动返回的字节数 NULL); // 同步调用不传 OVERLAPPED return bRet; }这个调用的要点在倒数第二个参数lpBytesReturned驱动处理完请求后会把实际消耗的字节数写到这里。如果驱动返回失败应用层应该用 GetLastError 读取具体错误码常见的是ERROR_INVALID_PARAMETER表示规则结构体长度不匹配ERROR_ACCESS_DENIED表示句柄权限不够。驱动端的控制码通常用 CTL_CODE 宏生成包含设备类型、功能号、访问权限和缓冲方法。#define FILE_DEVICE_FIREWALL 0x8000 #define IOCTL_FIREWALL_SET_RULES \ CTL_CODE(FILE_DEVICE_FIREWALL, 0x901, METHOD_BUFFERED, FILE_ANY_ACCESS)METHOD_BUFFERED 意味着系统会把输入缓冲从应用层拷贝到内核分配的系统缓冲区驱动直接操作这个系统缓冲区的指针即可不需要做内存映射。对于规则集这种一次性下发的数据缓冲方式最简单也最安全缺点是数据量大时多一次拷贝但对一个个人防火墙的场景完全够用。4.2 驱动内部的数据包处理链路DrvFltIp 的过滤入口在驱动初始化时挂入协议栈。取包后先解析出方向和协议字段再调用 MatchPacket 判断。驱动里拿到的包数据是原始 IP 报文的缓冲区解析时要小心字节序和首部长度IP 首部的protocol字段在偏移第九字节直接用((UCHAR*)pPacketData)[9]读取。端口在 TCP/UDP 首部里需要先按 IP 首部长度跳过头部再做偏移计算。NTSTATUS FirewallFilterPacket( PVOID pPacketData, PNET_PACKET_INFO pPacketInfo) { PCHAR pBuf (PCHAR)pPacketData; UCHAR ucVersionIhl pBuf[0]; ULONG ulHeaderLen (ucVersionIhl 0x0F) * 4; // 解析传输层端口 DumpTcpUdpPorts(pBuf, ulHeaderLen, pPacketInfo); if (MatchPacket(g_pRuleTable, g_ulRuleCount, pPacketInfo)) { return STATUS_SUCCESS; // 放行 } return STATUS_ACCESS_DENIED; // 丢弃 }驱动里维护规则表的方式与用户态略有区别。建议在驱动中保存一份规则数组和规则计数通过 PagedPool 分配内存保证 IRQL 较高时也能安全访问。更新规则时用互锁操作切换规则数组指针避免正在遍历规则的 DPC 例程读到被释放的内存。我一般会在这里加一个规则版本号每次下发递增查询状态时一并返回给应用层界面可以借此确认规则是真的换上了而不是调用失败。4.3 调试这个链路时看什么这套架构的常见故障点有三个设备句柄没打开成功、控制码和驱动内部分派不对齐、规则结构体长度不一致。第一个故障通过检查 TDriver 构造时的错误码定位第二个需要核对 TDriver.cpp 和 DrvFltIp.h 里的宏定义第三个直接用sizeof(FIREWALL_RULE)在两头各打一次值。调试驱动时如果手头没有 WinDbg 双机环境可以在驱动里用DbgPrint输出关键路径再配合 DebugView 看输出。应用层 ioctl 调用失败时先把dwBytesReturned打出来比如驱动期望规则条数是 100返回字节数是 0那多半是驱动内部匹配规则数时用错了计数。5. 编译构建与实测验证让过滤规则真正生效这个 VC6 工程要在新系统上重新构建编译顺序有讲究。先打开驱动子工程用对应的 DDK/WDK 环境变量编译出 DrvFltIp.sys再编译 FirewallApp 工程。如果用的是新版 Visual Studio旧的 .dsp 工程需要先转换成 .vcxproj转换后 MFC 工程的字符集设置要改成多字节否则 RuleDlg 里大量窄字符串 API 会编译报错。驱动工程的 WDK 版本按你当前 Windows 版本来不要追新过滤驱动涉及的旧接口在最新 WDK 里可能被移除。驱动加载在有签名校验的系统上会失败学习环境下可以开启测试签名模式。管理员命令行执行bcdedit /set testsigning on sc create FirewallDrv type kernel binPath C:\Firewall\DrvFltIp.sys sc start FirewallDrv重启后sc query FirewallDrv看到 STATE 为 RUNNING说明驱动挂载成功。随后启动 Firewall.exe在 RuleDlg 里加一条规则方向出站、协议 TCP、本地端口任意、远端端口写 135动作设为丢弃。应用后用同一台机器发起一条指向本机 135 端口的 TCP 连接握手阶段会直接卡住直至超时再清掉规则重试连接恢复正常。这个对比能确认三个环节是通的界面规则解析正确、IOCTL 下发没有报错、驱动里的 MatchPacket 真正拦截到了包。如果想进一步验证丢包动作在驱动过滤函数里临时加一个计数把命中次数通过另一个 IOCTL 读回来。界面收到非零计数的同时连接失败就能证明防火墙的完整闭环已经生效。后续把协议字段扩充到 ICMP 时记得while循环里检查 IP 首部协议字段再决定是否解析端口不然 ICMP 包没有端口可读匹配逻辑读到的是前一个包的残留数据。本文还有配套的精品资源点击获取