简介针对 Windows 10 x64 平台开发的 NDIS 6.0 Filter 驱动示例面向内核网络驱动开发人员与安全研究者演示如何基于 KMDF 框架扩展网卡数据包处理能力。工程在 Visual Studio 17.11.4 与 WDK 10.0.26100.1 环境下构建新增 OID 请求发送、ICMP 自定义数据包构造与发送、实时接收数据包等核心功能可帮助读者理解过滤器驱动挂载位置及与协议栈的交互机制。压缩包共 16 个文件其中 5 个 C 源文件与 5 个头文件按模块划分驱动逻辑、调试输出及数据结构定义另含工程配置、INF 安装脚本及演示程序整体仅 37KB便于快速查看源码结构。代码中可重点学习内核 OID 通信流程、内存分配与回收策略以及数据包收发路径的实现细节也附带查询网卡 MAC 地址的示例适合有一定驱动基础、想上手 NDIS Filter 开发的学习者。目前已有 139 人学习查看该资源。1. Win10 下的 NDIS 6.0 Filter 驱动到底能做什么从收发数据包和 MAC 查询说起基于 Win10 的 NDIS 6.0 Filter 驱动新增加了收发数据包以及查询网卡 MAC 地址的代码——这句话描述的是 NDIS 过滤驱动开发里最典型的一个起步组合。Filter 驱动位于网卡与协议栈之间既能观察每一笔出入流量也能在链路层截停、修改或放行数据包而 MAC 地址查询则是驱动初始化阶段最先要解决的“身份识别”问题。这套能力是流量监控、安全过滤、协议分析、网络管理类工具的地基。适合已经会写内核驱动、但还没碰过 NDIS 过滤框架的开发者或者被网上零散代码片段坑过的从业者。我不会堆理论直接把能跑通的工程逻辑和关键代码拆给你看。2. 开始前的准备WDK 工程、Filter 驱动骨架和绑定生命周期2.1 VS WDK 环境与 NDIS 版本选择为什么 Filter 模型能兼容 Win10先说环境。开发 NDIS Filter 驱动最常用的是 VS 开发环境加配套的 WDKWindows 驱动开发套件。工程类型选“内核模式空驱动”或者 WDK 自带的“NDIS Filter”模板都行。模板的好处是已经把 INF、入口函数和空回调生成好了省去手写大量定型代码的时间缺点是模板版本偏老和当前 WDK 的工程配置有差异需要手动调整链接器和 inf2cat 签名步骤。关于 NDIS 版本标题里写的 NDIS 6.0 是 Filter 驱动模型引入的版本起点。Win10 系统实际的 NDIS 主版本已经迭代到 6.40 以上但 NDIS 6.0 定义的过滤驱动框架——DriverEntry 注册回调、FilterAttach 绑定网卡、NdisF* 系列转发接口——直到 Win10 都保持稳定。所以用老项目的代码基础迁到 Win10 上重编译通常只需要处理头文件差异和 WDK 版本警告不需要把架构推倒重来。这个兼容性就是 Filter 相比 TDI、LSP 那类老方案在 Win10 上仍然值得做的原因。工程创建完以后INF 文件里的设备类要固定为网络类。一个简化但能安装的 Filter INF 大概长这样[Version] Signature $WINDOWS NT$ Class Net ClassGuid {4d36e972-e325-11ce-bfc1-08002be10318} DriverVer 01/01/2024,1.0.0.0 [DefaultInstall.NT] NetCfgInstanceId {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}INF 里的 ClassGuid 是网络设备类的固定标识不能用别的设备类 GUID 替换否则系统不会把它识别为可绑定的过滤驱动。NetCfgInstanceId 是每个网络实例的唯一 ID同一台机器装多块网卡、多个过滤实例时系统靠它区分实例上下文。如果这里少写 NetCfgInstanceId安装时会报“无法添加驱动程序”的错误这是第一个容易翻车的地方。2.2 驱动入口与回调注册哪些函数决定你“能收到包”Filter 驱动的入口函数和普通内核驱动一样是 DriverEntry但它做的事非常集中分配过滤驱动句柄初始化 NDIS_FILTER_DRIVER 结构体然后调用 NdisRegisterFilterDriver 完成注册。NDIS_FILTER_DRIVER 结构体里最关键的是四个回调FilterAttach 负责绑定网卡FilterDetach 负责解绑FilterReceiveNetBufferLists 负责收包FilterSendNetBufferLists 负责发包。收发函数没注册上后面的代码写得再多也看不到包。下面给出一个精简版的注册代码注意结构体字段很多这里只列了与收发相关的核心项实际编译时字段顺序以 WDK 头文件为准NDIS_FILTER_DRIVER g_FilterDriver; NDIS_HANDLE g_FilterDriverHandle; // DriverEntry 里做注册 NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NDIS_STATUS status; // 清零结构体避免残留指针 RtlZeroMemory(g_FilterDriver, sizeof(g_FilterDriver)); g_FilterDriver.MajorNdisVersion 6; g_FilterDriver.MinorNdisVersion 0; g_FilterDriver.FilterDriverProtocolMajorVersion 6; g_FilterDriver.FilterDriverProtocolMinorVersion 0; g_FilterDriver.FilterAttach FilterAttach; g_FilterDriver.FilterDetach FilterDetach; g_FilterDriver.FilterRestart FilterRestart; g_FilterDriver.FilterPause FilterPause; g_FilterDriver.FilterSendNetBufferLists FilterSendNetBufferLists; g_FilterDriver.FilterReceiveNetBufferLists FilterReceiveNetBufferLists; g_FilterDriver.FilterSendNetBufferListsComplete NULL; g_FilterDriver.FilterReceiveNetBufferListsComplete NULL; status NdisRegisterFilterDriver( DriverObject, RegistryPath, g_FilterDriver, g_FilterDriverHandle); return status; }这段代码里NdisRegisterFilterDriver 的第四参数是返回的驱动句柄卸载时要用它调用 NdisDeregisterFilterDriver。FilterSendNetBufferListsComplete 和 FilterReceiveNetBufferListsComplete 这两个完成回调暂时不注册不影响收发转发但如果你想统计“发出去的包是否成功”或者“接收是否被上层丢弃”这两个回调后面要补上。Filter 驱动不像网卡驱动那样自己发送接收它的职责是“站在中间传话”所以收发回调注册错位或者漏注册NDIS 不会报错只会静默地不调用你。2.3 FilterAttach/Detach/Restart/Pause收发代码该放哪一步注册FilterAttach 是实例绑定的起点。系统为每一块网卡创建一个过滤实例然后调用 FilterAttach。我的习惯是把每块网卡对应的绑定句柄、上下文、MAC 地址缓存、统计计数器全部放进一个自定义上下文结构体里再把这个结构体的指针作为 FilterModuleContext 传回给 NDIS。后续所有回调的第一个参数就是当年 FilterAttach 里穿进去的这个 Context。下面用表格说明这四个生命周期回调各自能做什么、不能做什么回调触发时机能做什么不能做什么FilterAttach与一块网卡完成绑定分配上下文、保存绑定句柄、缓存 MAC不能长时间阻塞不能从这里直接发 OID 并同步等待FilterDetach与网卡解绑释放上下文、清空计数不能假设还有包在飞FilterRestart进入运行态启动计数、重发 OID、恢复转发不能在这个回调里临时大块分配FilterPause暂停过滤停止计数、等待未完成 NBL 归位不能再调用 NdisFIndicateReceiveNetBufferLists这里有个新手常有的误解认为 FilterAttach 一旦执行收发回调就会立刻开始被调用。实际上 NDIS 在 FilterAttach 返回后还要经过暂停状态、重启状态驱动真正开始收发包是在 FilterRestart 完成之后。所以 FilterAttach 里只做资源准备不要在它里面发复杂 OID 请求或做长时间等待否则会影响系统枚举网络设备的速度甚至导致绑定超时。3. 收发数据包的实现Receive 与 Send 路径的落地代码3.1 ReceiveNetBufferLists接收路径的“接包”代码收包回调是 FilterReceiveNetBufferListsNDIS 每次可能把一个或多个数据包打包成一串 NET_BUFFER_LISTNBL链表传进来。处理这个链表是所有 Filter 收包逻辑的起点。下面这段代码把链路层数据长度和安全访问包数据要做的事都写在注释里你能直接对着抄VOID FilterReceiveNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG NumberOfNetBufferLists, ULONG ReceiveFlags) { PNET_BUFFER_LIST nbl NetBufferLists; PNET_BUFFER nb NULL; ULONG packetLen 0; // 遍历这一批 NBL while (nbl ! NULL) { nb NET_BUFFER_LIST_FIRST_NB(nbl); if (nb ! NULL) { // NET_BUFFER_DATA_LENGTH 是宏取的是整个 NET_BUFFER 的载荷长度 packetLen NET_BUFFER_DATA_LENGTH(nb); } // 在这里写你的过滤策略 // 统计长度、解析端口、决定放行还是拦截 // 取下一链表项 nbl NET_BUFFER_LIST_NEXT_NBL(nbl); } // 批量放行继续向上层协议栈传递 NdisFIndicateReceiveNetBufferLists( FilterModuleContext, NetBufferLists, PortNumber, NumberOfNetBufferLists, ReceiveFlags); }逻辑说明这段代码先遍历链表取每个包的 NET_BUFFER再通过 NET_BUFFER_DATA_LENGTH 宏拿到包长。过滤决策写在遍历循环里决策完成后统一调用 NdisFIndicateReceiveNetBufferLists 把整串 NBL 继续往上送。参数里 PortNumber 一般传 0除非你明确处理多端口网卡NumberOfNetBufferLists 必须和传入的链表节点数一致写错会导致 NDIS 内部链表计数错乱。ReceiveFlags 必须原样透传。它里面的 NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL 位表示当前回调是否运行在 DISPATCH_LEVEL这决定你能不能安全调用内存管理函数NDIS_RECEIVE_FLAGS_PER_PACKET_INFO 位表示每个 NBL 的带外信息是否独立有效。如果你需要拦截而不是放行不要调用 NdisFIndicateReceiveNetBufferLists改成调用 NdisFReturnNetBufferLists(FilterModuleContext, NetBufferLists, ReceiveFlags)包就会像“没被提交给上层”一样被归还给底层网卡驱动。这里一句话说清楚放行用 Indicate拦截用 Return二选一不要两个都调。3.2 SendNetBufferLists发送路径的“传包/挡包”代码发送路径的回调是 FilterSendNetBufferLists处理方式和接收路径高度对称。但它有一个非常关键的所有权差异接收路径里底层网卡把 NBL 交给 FilterFilter 有处置权发送路径里协议栈把 NBL 交给 FilterFilter 如果不转发就必须自己完成“发送完成”动作不能直接丢弃。VOID FilterSendNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG NumberOfNetBufferLists, ULONG SendFlags) { PNET_BUFFER_LIST nbl NetBufferLists; // 发送路径同样是批量处理 while (nbl ! NULL) { // 解析并决定放行 or 挡下 // 若放行继续遍历 nbl NET_BUFFER_LIST_NEXT_NBL(nbl); } // 默认动作整批放行下发 NdisFSendNetBufferLists( FilterModuleContext, NetBufferLists, PortNumber, NumberOfNetBufferLists, SendFlags); }逻辑说明NdisFSendNetBufferLists 把包交给底层网卡去发送它不会阻塞发送结果通过完成回调异步返回。如果想拦掉某个发送包不能直接释放 NBL也不能调用 NdisFSendNetBufferListsComplete 以外的手段。标准做法是标记要丢弃的包然后调用 NdisFSendNetBufferListsComplete 把它“完成”掉并返回失败状态。下面这段是拦截单个发送包的典型写法// 拦掉某个 NBL通知协议栈这个包“发送失败”它就不会被物理网卡发出 NdisFSendNetBufferListsComplete( FilterModuleContext, pNblToDrop, NDIS_STATUS_FAILURE, 0);参数说明第二个参数是你决定丢弃的单个 NBL第三个参数是完成状态上层协议栈会收到这个状态并认为发送失败第四个参数一般填 0。注意 NdisFSendNetBufferListsComplete 只能处理“确实由你拦下的包”已经通过 NdisFSendNetBufferLists 转下去的包不能再用它完成否则会造成 NBL 重复完成直接蓝屏。3.3 从 NBL 到实际数据解析包头的关键姿势上面代码只拿到了包长还没到数据内容。NDIS 的 NBL 底层是 MDL 链数据可能分布在多个 MDL 段上尤其网卡做了分片或者使用 DMA 环形缓冲时一个包的数据经常不在连续内存里。解析包头的第一步是把 MDL 映射到系统地址空间。PMDL mdl nb-FirstNetBufferMdl; PVOID addr NULL; ULONG segLen 0; // 遍历 MDL 链每段分别映射访问 while (mdl ! NULL) { addr MmGetSystemAddressForMdlSafe(mdl, LowPagePriority); segLen MmGetMdlByteCount(mdl); if (addr NULL) { // 映射失败说明当前 IRQL 不适合做这个操作 break; } // addr 指向这一段数据的起始位置 // 注意偏移真正的包数据从 NET_BUFFER_CURRENT_MDL_OFFSET 开始 mdl mdl-Next; }逻辑说明FirstNetBufferMdl 指向包的第一段数据但不保证整包只有一个 MDL。遍历时每一段都要单独调用 MmGetSystemAddressForMdlSafe得到虚拟地址后用 MmGetMdlByteCount 取段长。这里有个特别容易踩的坑FirstMdl 的起始地址是网卡 DMA 描述符的起始不一定等于以太网头起始位置实际数据起点要看 NET_BUFFER_CURRENT_MDL_OFFSET 宏。直接对 addr 做 pEth-Dest 这类偏移访问大概率读到错误数据。另一个更省事的做法是读 NBL 带外信息里的帧类型字段不需要手动解析以太网头。例如 NDIS_NET_BUFFER_LIST_INFO(nbl, NetBufferListFrameType) 可以直接拿到 0x0800IPv4、0x86DDIPv6、0x0806ARP这类帧类型。但它依赖网卡驱动是否正确填充该字段某些虚拟网卡不填拿到 0 就不要强行解析。这个信息我在第 6 章的验证技巧里还会用到。4. 查询网卡 MAC 地址OID 请求的完整写法4.1 为什么不能直接读寄存器而要发 OID_802_3_CURRENT_ADDRESSFilter 驱动不像 Miniport 驱动那样直接掌握网卡寄存器它拿到的是“绑定关系”。要 query 网卡 MAC 地址标准路径是通过 OIDObject Identifier向底层网卡驱动发起查询。最常用的是 OID_802_3_CURRENT_ADDRESS这个 OID 返回一个 NDIS_802_3_CURRENT_ADDRESS 结构里面的 CurrentAddress 字段就是 6 字节 MAC。注意区分两个概念OID_802_3_PERMANENT_ADDRESS 是网卡出厂固化的地址OID_802_3_CURRENT_ADDRESS 是当前正在使用的地址——网卡开了 MAC 地址伪装后两者会不一样Filter 驱动做绑定识别、白名单过滤应该用 CURRENT_ADDRESS不是 PERMANENT_ADDRESS。Filter 驱动发起 OID 请求和协议驱动类似都需要拿到一个绑定句柄。这个句柄在 FilterAttach 里通过 NdisOpenAdapterEx 得到并保存到我们自己的上下文结构体里typedef struct _FILTER_CONTEXT { NDIS_HANDLE FilterModuleHandle; NDIS_HANDLE NdisBindingHandle; UCHAR MacAddress[6]; ULONG ReceiveCount; ULONG SendCount; } FILTER_CONTEXT, *PFILTER_CONTEXT;NdisBindingHandle 在整个过滤实例生命周期里保持不变所有 OID 请求都用它。FilterDetach 时不需要手动关闭这个句柄NDIS 会随解绑过程统一释放自己调用 NdisCloseAdapterEx 反而可能造成二次关闭。4.2 发起 OID 请求与完成回调同步等待的实现OID 请求本质是异步的。NdisOidRequest 返回 NDIS_STATUS_PENDING 时表示请求挂起真正完成要等 NDIS 回调 FilterOidRequestComplete。想把它变成同步调用需要自旋锁加事件对象配合完成回调。由于 Filter 驱动里 IRQL 可能高于 PASSIVE_LEVEL事件等待不能放在 DISPATCH_LEVEL 下所以我一般把查询动作放到 FilterRestart 之后的工作线程里做线程里 IRQL 是 PASSIVE_LEVEL可以安全 KeWaitForSingleObject。下面是查询 MAC 的核心代码结构上做了精简重点看 OID 请求的构造和内存分配NTSTATUS QueryMacAddress( PFILTER_CONTEXT ctx, PUCHAR macOut) { NDIS_STATUS status; PNDIS_OID_REQUEST oidReq NULL; PVOID infoBuf NULL; ULONG bufLen sizeof(NDIS_802_3_CURRENT_ADDRESS); PNDIS_802_3_CURRENT_ADDRESS oidData NULL; // 分配 OID 请求结构和缓冲区优先用 NdisAllocateMemoryWithTagPriority oidReq NdisAllocateMemoryWithTagPriority( ctx-FilterModuleHandle, sizeof(NDIS_OID_REQUEST), doRO, LowPoolPriority); infoBuf NdisAllocateMemoryWithTagPriority( ctx-FilterModuleHandle, bufLen, diRO, LowPoolPriority); if (oidReq NULL || infoBuf NULL) { if (oidReq ! NULL) NdisFreeMemory(oidReq, 0, 0); if (infoBuf ! NULL) NdisFreeMemory(infoBuf, 0, 0); return STATUS_INSUFFICIENT_RESOURCES; } RtlZeroMemory(oidReq, sizeof(NDIS_OID_REQUEST)); RtlZeroMemory(infoBuf, bufLen); // 构造查询请求 oidReq-RequestType NdisRequestQueryInformation; oidReq-DATA.QUERY_INFORMATION.Oid OID_802_3_CURRENT_ADDRESS; oidReq-DATA.QUERY_INFORMATION.InformationBuffer infoBuf; oidReq-DATA.QUERY_INFORMATION.InformationBufferLength bufLen; // 发起请求完整代码里这里用事件等待 FilterOidRequestComplete status NdisOidRequest(ctx-NdisBindingHandle, oidReq); if (status NDIS_STATUS_PENDING) { // 等待完成回调置事件超时一般给 2-3 秒 // status WaitForComplete; } if (status NDIS_STATUS_SUCCESS) { oidData (PNDIS_802_3_CURRENT_ADDRESS)infoBuf; RtlCopyMemory(macOut, oidData-CurrentAddress, 6); } NdisFreeMemory(infoBuf, 0, 0); NdisFreeMemory(oidReq, 0, 0); return status; }逻辑说明OID 请求结构里的 InformationBuffer 是查询结果的承载区长度不能小于 NDIS_802_3_CURRENT_ADDRESS 结构体大小。发起请求用的句柄是 FilterAttach 保存的 NdisBindingHandle不是 FilterModuleHandle这两个句柄很容易搞混搞混的后果是 NDIS 返回无效设备请求。返回 NDIS_STATUS_PENDING 后必须等待完成回调不能直接释放请求结构和缓冲区否则完成回调回来访问的是野指针。从 Win10 较新的 NDIS 版本开始系统提供了 NdisSynchronousOidRequest 这样的同步接口可以省去事件等待。但如果你需要兼容老工程和旧驱动库事件等待仍然是更稳妥的方案。另外OID 请求缓冲区建议都用 Ndis 系列函数分配不要用 ExAllocatePoolWithTag 替代至少在这套代码里保持一致方便后面排查内存标签对应的问题。4.3 多网卡环境下区分 MAC 与绑定接口一台机器上装多块网卡时Filter 驱动会被加载多次每块网卡一个独立实例每个实例有自己独立的下上文结构体。查询 MAC 时结果只属于当前这个 NetLuid 对应的网卡所以需要把 MAC 存到上下文而不是全局变量。// FilterAttach 创建上下文后把绑定句柄保存进去 ctx-NdisBindingHandle NdisBindingHandle; // FilterRestart 或工作线程里查 MAC QueryMacAddress(ctx, ctx-MacAddress); // 后续收发包时按 ctx-MacAddress 判断这个包属于哪个接口这样做的好处是你在 Receive 回调里看到包时可以通过 FilterModuleContext 参数直接拿到对应的 MAC 地址用它做多接口区分。比如一个防火墙驱动要分别统计有线网卡和无线网卡的流量只要比较包对应的 ctx-MacAddress 和系统里记录的两块网卡 MAC就能在同一个驱动实例里精确分流而不是靠 NDIS 端口号猜测。5. 避坑Win10 下 NDIS Filter 驱动的 5 个翻车点5.1 抓不到包FilterModule 没有被正确附加回调静默失效现象驱动编译安装都成功但 Receive 和 Send 回调一个都没被调用或者只收到发送包收不到接收包。原因最常见的是 FilterAttach 返回失败导致 NDIS 没有创建过滤实例。FilterAttach 里任何一步出错——比如 NdisOpenAdapterEx 失败、分配上下文失败、甚至注册 NetPnPEvent 失败——NDIS 都会静默放弃这次绑定不会弹出任何窗口。也有一种情况是驱动装了但没绑定到你正在用的网卡上需要到网络适配器属性里看“筛选器”列表有没有你的驱动名。解决FilterAttach 里临时加 DbgPrint 输出每一步状态和返回值先用内核调试器确认这个回调真的被调用。确认被调用后再检查返回的 status 是不是 NDIS_STATUS_SUCCESS。绑定状态可以用系统自带网络管理命令确认也能通过设备管理器的网络适配器属性查看。5.2 蓝屏 0xD1MDL 越界访问和错误的内存优先级现象收发量大时系统蓝屏错误码 0xD1 或者 0x3E崩溃栈指向 MmGetSystemAddressForMdlSafe 或你解析包头的那一行。原因两个主要来源。第一在 DISPATCH_LEVEL 下用 NormalPagePriority 调用 MmGetSystemAddressForMdlSafe触发了分页错误第二你觉得 MDL 链第一段就覆盖了整个包直接按完整包的长度去读取越界访问了下一个 MDL 的地址空间。解决统一在 DISPATCH_LEVEL 下使用 LowPagePriority 映射并把耗时处理挪到工作线程或定时器里做访问数据前先用 MmGetMdlByteCount 取段长再做长度校验校验失败就只转发不解析。宁可少看几层协议不要为了多看一层把系统打蓝屏。5.3 改了数据包却没改长度校验和与抓包工具双双报警现象在 Receive 回调里修改了包内容比如改写 MAC 或 IP 地址结果上层应用收不到数据抓包工具显示包长度错误或者协议栈频繁重传。原因NDIS 传递的数据包长度信息分散在 NET_BUFFER 的 DataLength、DataOffset、MDL 段字节数和 NBL 的带外长度字段里。只改内容不更新这些长度字段上层计算包尾位置就会出错TCP 校验和也会失败。解决改数据要遵循三步调整 NET_BUFFER_DATA_OFFSET 宏对应的偏移、更新 NET_BUFFER_DATA_LENGTH、再用 NdisRetreatNetBufferDataStart 或 NdisAdvanceNetBufferDataStart 做前后空间调整。如果只是做透明转发和统计完全没必要改包直接原样透传最安全。真要改先克隆 NBL不要在原始包上原地操作否则和上层协议栈的引用计数打架。5.4 查询 MAC 返回无效状态OID 发出时机太早现象FilterAttach 里同步调用 NdisOidRequest 查 MAC返回 NDIS_STATUS_INVALID_DEVICE_REQUEST 或 NDIS_STATUS_NOT_ACCEPTED但同一份代码放到系统完全启动后再执行就是好的。原因FilterAttach 阶段网卡绑定还没完全进入可查询状态底层 Miniport 驱动可能还处于暂停态对 OID 请求不响应。同步等待事件在 Attach 里也容易死锁因为 NDIS 回调路径的特殊性。解决把 OID 查询放到 FilterRestart 完成之后通过 NDIS 工作项NdisQueueIoWorkItem投递到系统工作线程执行并在完成事件上设置 23 秒超时。超时后就算查询没回来也不要无限等置超时标志后继续初始化MAC 没查到就当零地址处理后续用工作项重试。5.5 多网卡环境包序混乱没有绑定接口上下文现象一台机器上有线、无线多块网卡时日志里统计的流量张冠李戴A 网卡的收包数跑到 B 网卡头上。原因Filter 驱动是每网卡实例化的但回调代码里如果直接把“当前网卡”写成一个全局变量——比如全局保存最近一次 FilterRestart 的上下文——高并发环境下多个实例交错执行回调全局变量就会被覆盖。解决所有状态都放在 FilterModuleContext 指向的上下文结构体里回调收到 FilterModuleContext 后立即转换成自己的上下文指针再做字段访问。不要为“方便”设置全局上下文指针NDIS 没有承诺同一时刻只有一个实例在跑。按接口上下文隔离是最省心的解法。6. 进阶验证这驱动到底“灵不灵”用性能计数器说话驱动写完不是装上就结束了关键是验证它真的在收发包、真的没给系统性能挖坑。我一般会给驱动内置两个轻量级计数器每接口收包总数、发送总数。用 KeQueryPerformanceCounter 计算每批 NBL 的处理耗时既能验证逻辑是否被调用又能暴露出性能瓶颈。LARGE_INTEGER freq, start, end; ULONG64 elapsedUs; KeQueryPerformanceCounter(start); // 处理 NBL 的代码段 ProcessNetBufferLists(nblList); KeQueryPerformanceCounter(end); // 计算这批包的处理耗时单位微秒 elapsedUs (end.QuadPart - start.QuadPart) * 1000000 / freq.QuadPart; // 超过阈值就 DbgPrint 出来比如 500 微秒 if (elapsedUs 500) { DbgPrint(NDIS_Filter: NBL batch slow, %llu us\n, elapsedUs); }这套验证有一个前提计数逻辑和耗时统计本身不能影响包的转发路径。计数器只做累加打印信息只在超过阈值时输出避免每个包都格式化字符串。日志 IO 在高频收包下会反过来拖垮驱动这是性能验证里最容易犯的错。第二个推荐验证点是帧类型判断。在收包路径里用 NDIS_NET_BUFFER_LIST_INFO 取帧类型对照抓包软件的结果看能否匹配UINT16 frameType NDIS_NET_BUFFER_LIST_INFO(nbl, NetBufferListFrameType); // 0x0800 IPv4, 0x86DD IPv6, 0x0806 ARP // 网卡不填充时返回 0要加防护这个字段的判断结果能快速告诉你驱动拿到的包和抓包软件看到的是不是同一批数据。我在调试模拟项目 X 时就靠这个字段抓到了问题——网卡驱动对某些 offload 包不填帧类型我一开始当成未知协议丢弃白丢了一批本该放行的流量。加防护判断之后才确认是底层硬件行为差异不是 Filter 逻辑问题。我现在的习惯是改任何 NDIS 驱动之前先在纸上画清楚 NBL 的所有权转移路径——这个包现在归谁、下一步交给谁、谁来负责完成、谁释放内存。这条线画不顺代码写得再漂亮也会在压力测试里翻车。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?