简介:一套围绕 Windows 过滤平台(WFP)的流量转发与监控实现源码包,面向具备 C/C++ 基础、希望在内核态或驱动层做网络数据包拦截、转发与统计的开发者,也可用于企业级网络监控系统的二次开发参考。压缩包共 155 个文件,主体为 65 个头文件与 52 个 C++ 源文件,另有 6 个 C 文件及 Visual Studio 工程配置等,便于直接编译调试或移植。整包仅 444KB,代码紧凑、结构清晰,尤其适合学习 WFP 框架下驱动拦截、数据包处理、流量统计等关键机制。目前已有 512 人浏览学习,可作为入门 WFP 网络监控开发的实用参考资料。通过研读源码,可以掌握流量定向转发、异常流量识别、连接级带宽统计等实现思路,为自建监控系统提供可落地的代码基础。
1. WFP 流量转发与监控:这套源码包能直接改出你要的网络监控系统
一台 Windows 服务器上想搞 WFP 流量转发和 WFP 流量监控,最头疼的不是写不出代码,而是不知道在哪一层动手。市面上的网络监控系统要么只做旁路抓包,要么只能看整机带宽,想定位到进程级、同时把指定流量转走,ETW 和性能计数器都使不上劲。这份源码包解决的就是这个中间地带:它基于 Windows Filtering Platform 的 callout 机制,把「转发」和「监控」做成了两个可拆分的内核模块外壳,用户态再配一个下发规则和读取统计的控制端,拼起来就是一个能用的流量监控系统。适合手里已有 C/C++ 和 WDK 基础、想直接改出一套属于自己的网络监控系统的开发者和安全工程师,拿回去改参数比从零写快得多。
第 1 行说到了这套包的定位,下面进入正题:先讲为什么选 WFP,再讲转发与监控分别怎么落地,最后把坑一次性说清。整个源码包按驱动 + 用户态控制端的结构组织,下文所有代码段都可以对照 rar 包里的工程找到对应文件,只是命名可能略作简化。
2. 为什么是 WFP:对比 TDI 与 NDIS Hook,把选型账一次算清
2.1 三条技术路线的本质差别
Windows 上做流量转发和监控,绕不开三套框架:老旧的 TDI、底层的 NDIS Hook、以及微软主推的 WFP。先给一张选型对照表,后面所有代码取舍都建立在这张表上:
| 方案 | 工作位置 | 过滤粒度 | 稳定性与维护成本 | 适合场景 |
|---|---|---|---|---|
| TDI 过滤驱动 | 传输层入口 | 连接级别,拿不到完整数据包内容 | 微软已标记弃用,驱动模型老,蓝屏黑匣子 | 古董系统维护、老代码兼容 |
| NDIS 中间层 / Hook | 网卡驱动边界 | 能拿到以太网帧,粒度最细 | 要自己处理分片、校验和、链路状态,协议栈差异大 | 商业防火墙常用,但开发周期长 |
| WFP callout | 网络栈各阶段 | 按 Layer 订阅,能拿包也能拿连接信息 | 微软官方主推,架构清晰,签名合规即可稳定运行 | 流量转发、流量监控、访问控制一体 |
| WinDivert / WinPcap | 用户态旁路 | 用户态旁路抓包与注入 | 免驱,但需要安装 NPCAP,改不了系统连接状态 | 抓包分析、轻量转发工具 |
当时我拿到这份资源包,第一眼看到 WFP callout 驱动的目录结构就知道选型是对的:它把「看包」和「管连接」分开,驱动里不需要自己去维护网卡绑定和链路状态,网络栈的各个阶段都开放了挂载点。这是 TDI 给不了的,也是 NDIS 中间层开发成本最高的部分。对做流量监控系统来说,WFP 把脏活都干了,你只需要回答三个问题:在哪个 Layer 看、看什么条件、看了之后干什么。
2.2 WFP 的四层关键概念:Layer、Filter、Callout、Injection
WFP 的模型可以压缩成一句话:Filter 决定哪些流量进 Callout,Callout 决定对这些流量做什么,Injection 是 Callout 动手改包后的投递通道。Layer 是它们共同的舞台。
资源包驱动里实际用到的 Layer 主要有这几个,理解它们对后续改参数至关重要:
- FWPM_LAYER_ALE_AUTH_CONNECT_V4:TCP/UDP 连接建立时的授权点。这个层能拿到发起连接的进程 ID 和完整五元组,是做会话表、转发决策的首选位置。
- FWPM_LAYER_IP_FORWARD_V4:路由转发点,也就是系统决定把包从哪个网卡发出去的时机。做网关型转发经常挂在这里。
- FWPM_LAYER_STREAM_V4:TCP 数据面。能拿到重排后的流数据片段,适合做流量统计,不适合做按包改址。
- FWPM_LAYER_OUTBOUND_NETWORK_V4 / INBOUND_NETWORK_V4:贴近网卡的包级出入口,能看到完整的 IP 包,但拿不到进程信息。
很多人在 WFP 里翻车,不是代码写错,而是挂错了层。比如想在连接建立时按进程转发,却把 Filter 挂到 OUTBOUND_NETWORK 层,结果 processId 永远是 0。这套源码包里转发和统计分别挂了不同的层,正是这个原因。
2.3 这套源码包的模块与工作流
解压 rar 后,典型的工程布局是:一个内核驱动工程(.sys),负责注册 callout、做转发和统计;一个用户态控制端工程(.exe),负责打开引擎、下发 Filter、读出统计;再加一套构建脚本和说明文档。
工作流大致是这样:控制端启动后通过 Filtering Platform 的用户态 API 把「哪个进程、哪个目标地址要被转发」下发给引擎;驱动里的 callout 收到匹配流量后克隆包、改目标地址、注入到指定方向;同时另一组 callout 在流层按连接累计字节数,写入 per-flow 结构体;控制端再通过 DeviceIoControl 周期把统计取走,刷新到界面或写日志。
这个结构的好处是转发和监控两个功能解耦,你可以只保留监控、去掉转发,也可以反过来只做转发。后面第 3 章和第 4 章分别拆这两条线,第 5 章再把合在一起时的坑讲透。
3. 流量转发落地:Callout 分类函数里的克隆、改址与注入
3.1 驱动入口:先注册转发 Callout,再看怎么下发规则
转发功能的核心是一个注册在 ALE_AUTH_CONNECT_V4 层的 callout。驱动入口先创建注入句柄,再注册 callout,最后把 callout ID 暴露给用户态,让控制端按 ID 绑定 Filter。代码骨架如下:
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING regPath) { FWPS_CALLOUT callout = {0}; NTSTATUS status; // 1. 创建网络层注入句柄,后面所有克隆包的注入都要用它 status = FwpsInjectionHandleCreate( AF_INET, FWPS_INJECTION_TYPE_NETWORK, &gInjectHandle); if (!NT_SUCCESS(status)) { return status; } // 2. 注册 callout,分类函数负责转发决策,flowDelete 负责清理会话 callout.flags = 0; callout.classifyFn = ClassifyRedirect; callout.notifyFn = NotifyFn; callout.flowDeleteFn = FlowDeleteFn; status = FwpsCalloutRegister(driver, &callout, &gRedirectCalloutId); if (!NT_SUCCESS(status)) { FwpsInjectionHandleDestroy(gInjectHandle); return status; } // 3. 记录 callout key,用户态通过 GUID 找到它 gRedirectCalloutKey = REDIRECT_CALLOUT_KEY; return STATUS_SUCCESS; }逻辑说明:FwpsInjectionHandleCreate 的参数中,AF_INET 表示只处理 IPv4,FWPS_INJECTION_TYPE_NETWORK 表示注入类型是网络层注入。FwpsCalloutRegister 把 classify、notify、flowDelete 三个回调绑进框架,其中 classifyFn 是最核心的,它决定了每一个匹配到的包怎么处理。这里的 gInjectHandle 是全局句柄,必须在驱动卸载时销毁,否则卸载后内核里会挂着无效句柄。
参数说明:callout.flags 一般保持 0;classifyFn 不能为 NULL,否则驱动加载后在过滤路径上会直接抛异常。flowDeleteFn 是流结束时的清理回调,统计模块会在第 4 章用到它,转发模块里主要用于释放节点,避免后续内存泄漏。
3.2 分类函数里的关键时刻:克隆、改址、注入
classify 函数的职责是:判断当前包是否需要转发,如果命中了配置的规则,就克隆一份包、修改头部目标地址、用注入 API 把它投递到本机指定端口。注意,这里不是拦截原包,而是让原包继续走,克隆包走转发路径。这样业务连接本身不受影响,只是多了一份副本被导到调试服务。
void ClassifyRedirect( const FWPS_INCOMING_VALUES* inFixedValues, const FWPS_INCOMING_METADATA_VALUES* inMetaValues, void* layerData, const void* classifyContext, const FWPS_FILTER* filter, UINT64 flowContext, FWPS_CLASSIFY_OUT* classifyOutput) { UINT32 pid = 0; UINT32 remoteAddr = 0; // 关键:先把注入态排除掉,否则注入的克隆包会再次触发自己 if (FwpsQueryPacketInjectionState( gInjectHandle, layerData, NULL) != FWPS_PACKET_NOT_INJECTED) { classifyOutput->actionType = FWP_ACTION_PERMIT; return; } // 只有请求了 metadata 字段时才能拿到 PID if (inMetaValues->currentMetadataValues & FWPS_METADATA_FIELD_PROCESS_ID) { pid = inMetaValues->processId; } remoteAddr = inFixedValues->layerFields[FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS].value.uint32; // 命中转发规则:克隆、改址、注入 if (ShouldRedirect(pid, remoteAddr)) { NET_BUFFER_LIST* nblClone = NULL; NTSTATUS status = FwpsAllocateCloneNetBufferList( (NET_BUFFER_LIST*)layerData, NULL, NULL, 0, &nblClone); if (NT_SUCCESS(status)) { ModifyPacketDestination(nblClone, gRedirectTargetIp, gRedirectTargetPort); FwpsInjectNetworkReceiveAsync( gInjectHandle, NULL, 0, nblClone); } } // 原包永远放行,转发不影响正常通信 classifyOutput->actionType = FWP_ACTION_PERMIT; }逻辑说明:FwpsQueryPacketInjectionState 检查当前包是否由本驱动注入,这是防止无限递归的第一道防线。FwpsAllocateCloneNetBufferList 克隆的是 NET_BUFFER_LIST 结构,不是 memcpy 整个数据包,所以成本可控。ModifyPacketDestination 是本资源的辅助函数,它改写 IP 头目的地址和端口,并重新计算 IP 校验和与 UDP/TCP 校验和;用 FwpsInjectNetworkReceiveAsync 而不是 Send,是因为目标地址是本机 127.0.0.1,网络层接收注入才能让回环包正常进入本地协议栈。
参数说明:gRedirectTargetIp 和 gRedirectTargetPort 是编译期写死的默认转发目标,实际运行时这些值由用户态通过 DeviceIoControl 动态下发。clone 包注入失败时,需要调用 FwpsFreeCloneNetBufferList 释放,否则每丢一个包就漏一块非分页内存,长时间运行会触发内存压力。
3.3 用户态下发规则:Filter 才是真正的条件闸门
驱动里的 callout 不会自己跑起来,必须有用户态在指定 Layer 上挂一条 Filter,把流量引到 callout。这套源码包控制端的核心逻辑是:打开引擎、开启事务、组装条件、加 Filter、提交事务。
HANDLE engine = NULL; NTSTATUS status = FwpmEngineOpen(NULL, RPC_C_AUTHN_WINNT, NULL, NULL, &engine); if (!NT_SUCCESS(status)) return; status = FwpmTransactionBegin(engine, NULL); FWPM_FILTER filter = {0}; filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; filter.displayData.name = L"Redirect Rule"; filter.action.type = FWP_ACTION_CALLOUT_TERMINATING; filter.action.calloutKey = REDIRECT_CALLOUT_KEY; filter.numFilterConditions = 2; filter.filterCondition = conditions; // conditions[0]: FWPM_CONDITION_ALE_APP_ID,进程路径,类型 FWP_BYTE_BLOB // conditions[1]: FWPM_CONDITION_IP_REMOTE_ADDRESS,目标地址,类型 FWP_UINT32 status = FwpmFilterAdd(engine, &filter, NULL, &ruleId); status = FwpmTransactionCommit(engine);逻辑说明:layerKey 指定在 ALE_AUTH_CONNECT_V4 层监听;FWP_ACTION_CALLOUT_TERMINATING 表示让 callout 的 classify 决定最终动作,而不是简单地放行或阻止。conditions 是条件数组:第一条用 ALE_APP_ID 匹配进程路径,第二条用 IP_REMOTE_ADDRESS 匹配目标网段。两个条件用 AND 语义叠加,只有同时命中的流量才进 callout。
参数说明:WFP 不支持直接用 PID 做过滤条件,PID 只在 metadata 里给 callout 读取。想精确匹配某个进程,标准做法是把进程全路径转成 FWP_BYTE_BLOB。注意路径大小写必须与实际一致,很多规则失效的案例都是路径里反斜杠或大小写不匹配导致的。另外,Filter 的优先级由 weight 决定,默认权重足够用,但如果你同时挂了多条转发规则,建议显式设置 filter.weight,避免两条规则抢流量时行为不确定。
4. 流量监控做实:per-flow 计数、进程归属与用户态读取
4.1 统计结构体:一张散列表管住所有连接
流量监控要回答的核心问题是:这台机器上哪个进程在跑流量、跑了多少、目标是谁。驱动里最常见做法是维护一张 per-flow 散列表,key 是五元组,value 是统计结构体。classify 每收到一个流数据片段,就查表累加。
typedef struct _FLOW_STATS { UINT64 processedBytes; // 累计 payload 字节数 UINT64 lastSeenTicks; // 最后一次活跃时间,用于超时清理 UINT32 pid; // 进程 ID UINT32 remoteAddr; // 远端地址 UINT32 remotePort; // 远端端口 } FLOW_STATS;这段结构体是监控系统的地基。processedBytes 记录累计字节数;lastSeenTicks 由 KeQueryTickCount 得到,用于用户态判断一个连接是否已超时;pid 要在流层拿,但流层不一定有进程元数据,于是资源包做法是在 ALE 层的 callout 里查好 PID 后写入会话表,stream 层查表拿到。这种方式比在每个包上都去过滤 metadata 效率高得多。
4.2 流层 classify:累加计数最怕抢锁
统计类 callout 注册在 FWPS_LAYER_STREAM_V4,此时 layerData 是 FWPS_STREAM_DATA,里面有当前流片段的 dataLength。收到片段后,按五元组查表,查到就累加。由于 classify 可能同时在多个 CPU 上并发执行,累加必须用 Interlocked 系列原子操作,不能直接stats->processedBytes += len。
static FLOW_STATS* GetOrCreateFlowStats( const FWPS_INCOMING_VALUES* inFixedValues, const FWPS_INCOMING_METADATA_VALUES* inMetaValues) { FLOW_STATS* stats = NULL; FLOW_KEY key = {0}; key.remoteAddr = inFixedValues->layerFields[FWPS_FIELD_STREAM_V4_IP_REMOTE_ADDRESS].value.uint32; key.remotePort = inFixedValues->layerFields[FWPS_FIELD_STREAM_V4_IP_REMOTE_PORT].value.uint16; key.localPort = inFixedValues->layerFields[FWPS_FIELD_STREAM_V4_IP_LOCAL_PORT].value.uint16; stats = LookupFlowStats(&key); if (stats == NULL) { stats = ExAllocatePoolWithTag(NonPagedPoolNx, sizeof(FLOW_STATS), 'FTSW'); if (stats != NULL) { RtlZeroMemory(stats, sizeof(FLOW_STATS)); stats->pid = GetPidFromSessionTable(inFixedValues, inMetaValues); InsertFlowStats(&key, stats); } } return stats; }逻辑说明:LookupFlowStats 用自旋锁保护的散列表查节点,查不到就分配新节点并填充 PID。这里把 key 缩小为「远端 IP + 两端端口」,因为对绝大多数流量监控需求来说,协议类型和本机 IP 不会影响统计归属,少两个字段能显著降低散列冲突。需要注意,统计对象是 TCP 流,不是 IP 包,所以入参字段用的是 STREAM_V4 层字段名。
参数说明:ExAllocatePoolWithTag 的 Tag 'FTSW' 是四个可见字符,用于驱动卸载时排查内存泄漏。NonPagedPoolNx 在 WDK 10 中推荐替代 NonPagedPool。流结束时的清理动作放在 flowDeleteFn 回调里,删除散列节点并释放内存。
流层 classify 里累加的逻辑很短,但有个隐藏点:这里统计的是 payload 字节数,不含 IP 头和 TCP 头。如果你想看到「网卡上的真实字节数」,要自己在用户态按平均头长折算,或者改挂到 OUTBOUND_NETWORK 层统计完整 IP 包长度。两种口径各有拥趸,做网络监控系统我一般以流层 payload 为准,因为业务流量统计通常关心的是应用层数据量,而且不容易被分片干扰。
4.3 用户态读取:DeviceIoControl 与结构体对齐
内核往统计表里写,用户态怎么拿?这套包采用最直接的 DeviceIoControl 方式:控制端周期性调用一次 IOCTL,内核把当前所有活跃连接统计拷到 output buffer,用户态解析并展示。内核里填充的规则是:先拷一个 count,再跟着 count 个 FLOW_STATS 结构体。
typedef struct _FLOW_STATS_IOCTL_OUT { ULONG count; FLOW_STATS entries[MAX_FLOW_ENTRIES]; } FLOW_STATS_IOCTL_OUT; FLOW_STATS_IOCTL_OUT out = {0}; DWORD bytesReturned = 0; BOOL ok = DeviceIoControl( hDevice, IOCTL_WFP_GET_FLOW_STATS, NULL, 0, &out, sizeof(out), &bytesReturned, NULL); if (ok && bytesReturned >= sizeof(ULONG)) { for (ULONG i = 0; i < out.count; i++) { FLOW_STATS* s = &out.entries[i]; printf("pid=%u remote=%u.%u.%u.%u:%u bytes=%llu\n", s->pid, (s->remoteAddr >> 24) & 0xFF, (s->remoteAddr >> 16) & 0xFF, (s->remoteAddr >> 8) & 0xFF, s->remoteAddr & 0xFF, s->remotePort, s->processedBytes); } }逻辑说明:DeviceIoControl 的 input buffer 传 NULL,因为本次只读不写;output buffer 要求固定大小。内核驱动里对应的 IRP_MJ_DEVICE_CONTROL 处理函数会先检查 output buffer 长度是否够,再填充数据。用户态拿到 count 后,逐一格式化输出。
这里最实战的经验是结构体对齐。FLOW_STATS 里有 UINT64 字段,Windows 上默认 8 字节对齐,如果结构体里字段顺序不当,内核态和用户态编译器对齐规则不一致,拷贝过来 remotePort 可能整体错位。解决方法是把 UINT64 字段放最前面,后面全部是 UINT32 和 UINT16,并用#pragma pack保持两边一致。看起来是小事,但这类字段偏移错误排查起来极费时间。
5. WFP 开发避坑:签名、注入循环、回环与卸载的五条血泪记录
5.1 驱动装不上:开始服务后报 577,代码毫无问题
现象:用 sc create 创建服务成功,sc start 直接失败,系统事件日志里看到驱动程序返回 577,或者提示「无法验证此文件数字签名」。在开发机上最典型,因为我们用的是 Debug 构建的 .sys。
原因:64 位 Windows 默认强制加载有 WHQL 签名的驱动,Debug 构建的 WFP 驱动没有签名,系统拒绝加载。这不是代码问题,是签名策略问题。
解决:开发环境打开测试签名模式后重启,然后在管理员的 cmd 里执行 bcdedit,生效后再重新加载驱动。
bcdedit /set testsigning on shutdown /r /t 0重启后桌面右下角有「测试模式」水印,属于正常现象。真正要分发时再做 WHQL 或微软 attestation 签名。不要为了绕过签名去动系统保护机制,那也是给自己挖坑。
5.2 注入的包又触发自己的 Callout:转发风暴和 CPU 拉满
现象:只转发一条连接,结果驱动所在进程 CPU 长期 100%,统计计数暴涨到十亿级,行为完全失控。
原因:克隆包注入回网络栈后,会再次经过同一条 Filter,再一次进入 ClassifyRedirect。如果没有注入态判断,就形成「匹配 → 克隆注入 → 再匹配 → 再克隆注入」的无限循环。
解决:classify 函数第一行就调用 FwpsQueryPacketInjectionState,只要不是 FWPS_PACKET_NOT_INJECTED,直接放行返回。代码第 3 章已经体现。这条判断是 WFP 开发里最重要的保护,没有之一,凡是做注入的 callout 都必须先过这关。
5.3 转发目标是 127.0.0.1,包却消失不见
现象:把 ModifyPacketDestination 的目标改成 127.0.0.1:8080 后,目标服务始终收不到任何数据,原包转发正常,但克隆包像被丢进黑洞。
原因:发送方向的注入句柄对应的是出站网络路径,而本机回环地址的数据包需要进入本地接收路径。用 FwpsInjectNetworkSendAsync 注入一个目的地址为 127.0.0.1 的包,协议栈不认它为本地投递,直接丢弃。这是 WFP 初学者最容易犯的错,微软文档里其实写得很清楚,只是英文文档没人逐句读。
解决:转发到本机统一用 FwpsInjectNetworkReceiveAsync 做接收注入,注入 API 的 semantic 要和目标地址方向匹配。后来我习惯在驱动里封装一个 InjectToLocal 函数,内部固定调 Receive 注入,从根上避开这个坑。如果你要把包转到另一台机器而不是本机,才走 Send 注入路径。
5.4 卸载驱动蓝屏:问题多半在清理顺序
现象:驱动安装、转发、监控都正常,一卸载立刻蓝屏,dump 指向 callout classify 函数地址,重启后一切恢复,再次卸载再次蓝屏。这是给测试同事留下心理阴影的典型故障。
原因:驱动卸载时,如果还有 flow 上下文没清理,或者还有线程正在 classify 内部执行,FwpsCalloutUnregister 返回后回调函数地址仍然可能被引用,此时 DriverUnload 释放了驱动映像,网络栈某个 DPC 里跳进已卸载的内存,蓝屏就是必然。
解决:卸载顺序严格按三步走:先在用户态 FwpmFilterDeleteById 删掉所有规则,确保没新流量再进 callout;再在驱动里 FwpsCalloutUnregister,等框架确认回调不再被调用;最后 FwpsInjectionHandleDestroy 销毁句柄。flowDeleteFn 里把散列表节点清掉,不要在 DriverUnload 里直接 ExFreePool 所有统计节点。顺序写反的驱动,蓝屏概率极高。
5.5 流量统计翻倍:上下行重复计数与注入包干扰
现象:用网卡计数器校验,发现监控系统统计的字节数比真实流量多 30% 到 100%,时准时不准,没有稳定规律。
原因:第一个来源是上下行各挂了一层统计 callout,同一份 TCP 内容在发送和接收路径各计一次;第二个来源是转发模块的克隆包又走回来,虽然它被放行,但如果统计 callout 没做注入态检查,会把克隆包的流量也算进去。
解决:统计模块同样做注入态过滤,并且明确统计口径:要流量监控系统的「业务流量」,就只统计连接发起方向,另一个方向用对端回包计数单独记录;要全链路字节数,就选 INBOUND/OUTBOUND 其中一个方向统一计。定了口径再做校验,数字才能对上。另外,sp3 之后的 WFP 上,分片重组包的 dataLength 和原始包不同,不要试图逐个包还原 payload,直接按流层累计最稳。
6. 最后一公里:把 WFP 转发与监控拼成一个能跑的系统
6.1 拼装时先定好三个绑定关系
把第 3 章和第 4 章的代码合到一起,首先要明确内核与用户态的契约。这张表写清楚,后面改起来才不用两头查:
| 契约项 | 说明 |
|---|---|
| 设备名与符号链接 | 控制端 CreateFile 打开的设备路径,固定字符串 |
| IOCTL 码 | 转发规则设置、统计读取各占一个 IOCTL_CODE,用 CTL_CODE 宏定义 |
| 结构体布局 | FLOW_STATS、RULE_SETTING 的字段顺序与对齐两边完全一致 |
| 规则下发的同步语义 | 每次配置变更后,控制端做一次 FwpmFilterAdd,驱动端不额外缓存 |
拼装时我采用的方式是控制端先打开设备,拿到驱动版本号,然后下发规则、读统计。顺序不能反过来,否则先读统计时内核还没有初始化散列表,返回空列表容易误判成采集失败。
6.2 验证方法:用一条真实连接走完整个链路
拿到这套源码包后,别急着改功能,先把基本链路跑通。我会用一条 curl 命令同时验证转发和监控:在控制端配置一个测试进程路径的转发规则,目标指向本地 8080 端口;然后从该进程发起一次 HTTP 请求,同时观察控制端界面。
curl.exe http://192.168.10.20/test -o nul正常情况下,控制端会看到一个 PID 正在上行且字节数持续增长,同时本地 8080 调试服务收到一个目标地址被改写过的请求副本。原请求本身仍然返回成功,因为原包没有被拦截。如果请求返回失败,说明转发逻辑把原包影响了;如果本地 8080 没收到,优先检查注入态和注入方向这两条避坑记录。
我一般还会配合 PowerShell 的 Get-NetUDPEndpoint 主动确认端口监听状态,驱动层面则索要一份运行日志。日志里每命中一条规则就打一行 DbgPrint,用 DbgView 观察,能大大缩短排查路径。
6.3 这份源码包值得改的三个点
第一个值得改的是把转发目标从代码写死改成配置表。用 JSON 或注册表存一份规则列表,控制端启动时读入并批量下发,比每改一次目标就重编一次驱动舒服得多。
第二个是用户态读取统计改事件驱动。现有 DeviceIoControl 轮询方式简单可控,但频繁调用会浪费 CPU;可以改成驱动在统计表变化超过阈值时触发一个通知事件,控制端通过 WaitForSingleObject 等待,省掉空轮询。
第三个是补 IPv6 支持。这套包默认只处理 IPv4,条件字段都是 V4 后缀。把 Layer 换成 V6 版本、地址字段改成 16 字节类型后,转发和监控可以平滑支持 IPv6 网络,改动量集中在一个头文件里,是性价比最高的扩展。
从那以后我每次拿到 WFP 相关源码包,第一件事都是打开 DriverUnload 检查清理顺序,然后在每个 classify 第一行确认注入态判断,最后再用一条 curl 把转发和统计同时过一遍。这三步走通了,这个资源才算真正到手。希望帮到你。
本文还有配套的精品资源,点击获取