简介:这份TDI驱动源代码面向Windows内核驱动开发者与网络监控方向的学习者,核心目标是在应用层实时动态获取每个进程打开的网络端口变化情况。为兼容Win7与WinXP,作者采用TDI框架自行搭建代码结构,未借用tdifw等现成源码,并额外实现了进程流量监控、单连接流量统计以及简单的进程禁网功能,限速部分留待扩展。资源包共68个文件,约567KB,包含8个cpp与7个h源文件、2个sys驱动、2个c文件、若干obj与pdb编译产物,以及xml、log、makefile、sources、dsp等工程配置,覆盖32位与64位Win7、WinXP的构建与测试环境。目前已有530人学习下载。读者可从中获得一套完整的TDI驱动开发框架、应用层接口代码与双平台编译配置,理解360流量防火墙限速功能的核心实现思路,并在此基础上继续扩展限速与更完善的应用层开发。
1. TDI驱动源代码:从网络过滤到协议栈挂钩的完整实现
做过Windows内核网络过滤的同行多半有过这种体验:想拦一个TCP连接,WFP的callout注册流程长得让人头皮发麻,NDIS又偏底层,改起来牵一发动全身。TDI驱动源代码这份资源,走的是另一条路——在传输层驱动接口(Transport Driver Interface)这一层做文章,直接和TCP/IP协议栈对话。它解决的核心问题是:如何在不依赖WFP框架的前提下,拿到网络连接级别的控制权,包括连接建立、数据收发、地址绑定这些关键节点。适合谁?适合已经能写基础WDM驱动、想深入理解Windows网络内核路径的C++开发者,也适合需要做轻量级网络行为监控、连接审计、协议过滤的从业者。这份代码不是玩具,它把TDI客户端驱动的完整骨架摊开了给你看,从DriverEntry到IRP派发,从设备对象创建到事件回调注册,每一块都能对上号。
2. TDI驱动的工作机制:为什么选它而不是WFP
2.1 TDI在Windows网络栈中的位置
要理解这份代码的价值,得先搞清楚TDI到底站在哪。Windows网络栈从下往上大致是:网卡驱动 → NDIS → TCP/IP协议驱动 → TDI接口 → 上层网络API(Winsock、NetBIOS等)。TDI不是驱动本身,而是一组内核态接口规范,它定义了传输协议驱动(比如tcpip.sys)向上层客户端驱动暴露的调用约定。你写一个TDI客户端驱动,本质上是把自己挂到tcpip.sys这个传输驱动上,通过它提供的设备对象(\Device\Tcp、\Device\Udp、\Device\RawIp)来发起或拦截网络操作。
这份源代码的核心思路就是:创建一个TDI客户端驱动,在DriverEntry里绑定到TCP设备,注册事件回调,然后通过IRP与底层协议驱动通信。和WFP相比,TDI的路子更“原始”,没有WFP那套分层过滤器和callout的抽象,但胜在直接、可控,代码量小,调试路径短。
2.2 选型理由:TDI vs WFP vs NDIS
| 对比维度 | TDI | WFP | NDIS |
|---|---|---|---|
| 介入层级 | 传输层接口 | 网络层/传输层过滤框架 | 数据链路层 |
| 开发复杂度 | 中等,需理解IRP和事件回调 | 较高,callout和过滤器管理复杂 | 高,需处理包和缓冲区 |
| 适用场景 | 连接级监控、协议过滤 | 防火墙、流量整形 | 网卡级抓包、协议驱动 |
| 微软支持状态 | 旧版接口,仍可用 | 官方主推 | 官方主推 |
| 代码可控性 | 高,路径短 | 中,框架封装多 | 低,细节多 |
选TDI的理由很实在:如果你只需要在连接层面做文章,比如记录谁连了谁、拦某个特定端口的握手、审计TCP连接的生命周期,TDI的代码量和理解成本比WFP低一个量级。WFP的callout注册、过滤器添加、层绑定那一套流程,没个几千行下不来,而TDI客户端驱动核心逻辑几百行就能跑起来。当然,代价是TDI属于较老的接口,新系统上虽然还能用,但微软不再更新,长期维护要考虑迁移路径。
2.3 核心数据结构与事件模型
TDI客户端驱动的骨架围绕几个关键结构展开。首先是TDI_ENTITY和TDI_CLIENT_EVENT_HANDLERS,前者标识你绑定的传输实体(TCP/UDP/RawIP),后者是一组回调函数指针,用来接收连接建立、断开、数据到达等事件。代码里通常会看到这样的初始化流程:
// 定义TDI客户端事件处理结构 TDI_CLIENT_EVENT_HANDLERS g_EventHandlers = {0}; // 设置连接建立回调 g_EventHandlers.ConnectionHandler = TdiConnectionHandler; // 设置断开连接回调 g_EventHandlers.DisconnectHandler = TdiDisconnectHandler; // 设置接收数据回调 g_EventHandlers.ReceiveHandler = TdiReceiveHandler; // 打开TCP设备对象 UNICODE_STRING tcpDeviceName; RtlInitUnicodeString(&tcpDeviceName, L"\\Device\\Tcp"); OBJECT_ATTRIBUTES objAttr; InitializeObjectAttributes(&objAttr, &tcpDeviceName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); HANDLE tcpHandle; NTSTATUS status = ZwCreateFile(&tcpHandle, GENERIC_READ | GENERIC_WRITE, &objAttr, &ioStatus, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN, 0, NULL, 0);这段代码做了两件事:一是填充事件回调表,告诉TDI层“有连接事件时调用我这些函数”;二是打开TCP设备对象,拿到一个句柄,后续所有IRP都通过这个句柄发下去。参数上,OBJ_KERNEL_HANDLE确保句柄只在核心态可见,FILE_OPEN表示打开已有设备而非创建。ZwCreateFile返回的tcpHandle是后续TdiBuildInternalDeviceControlIrp等调用的基础。
事件模型是异步的:你注册回调,TDI层在合适的时候调用你。这意味着你的回调函数运行在任意线程上下文,可能在高IRQL,不能做分页内存操作,不能随便等锁。这是新手最容易翻车的地方——在回调里调KeWaitForSingleObject直接蓝屏。
3. 编译与加载:从源码到可运行驱动
3.1 环境准备与项目配置
这份源代码是C++写的WDM驱动,编译需要WDK(Windows Driver Kit)。我一般用Visual Studio + WDK的集成环境,新建一个“Empty WDM Driver”项目,然后把源码文件加进去。关键配置项有几个:目标平台选Desktop,目标OS版本根据你的测试机来,驱动类型选WDM。项目属性里要确保C/C++ → 常规 → 将警告视为错误关掉,因为TDI的旧头文件里有些弃用警告会卡编译。
源码里通常包含这几个文件:DriverEntry.cpp(入口)、TdiClient.cpp(TDI绑定与IRP处理)、Callbacks.cpp(事件回调实现)、TdiClient.h(结构定义)。如果你拿到的包里有.inf文件,那是安装驱动用的,没有的话可以手动用sc create注册。
3.2 编译命令与常见报错处理
用命令行编译的话,在WDK的build environment里跑:
# 进入源码目录 cd /d C:\Projects\TdiDriverSource # 使用WDK的msbuild编译 msbuild TdiDriver.vcxproj /p:Configuration=Release /p:Platform=x64 # 如果报找不到tdi.h,检查WDK版本和包含路径 # 通常需要包含 $(WDKContentRoot)\Include\$(TargetPlatformVersion)\km编译时最常见的报错是tdi.h找不到或者TDI_ENTITY未定义。原因是WDK版本差异——新版WDK把TDI头文件挪到了km\crt或者需要额外包含wdm.h之前先定义TDI_VERSION。解决办法是在包含tdi.h之前加:
#define TDI_VERSION 0x0200 #include <tdi.h>另一个坑是链接错误unresolved external symbol TdiBuildInternalDeviceControlIrp。这个函数在tdi.lib里,需要在项目里链接这个库。VS里在链接器 → 输入 → 附加依赖项加上tdi.lib,或者代码里用#pragma comment(lib, "tdi.lib")。
3.3 驱动加载与验证
编译出.sys文件后,加载方式有两种:一是用sc create注册服务,二是用工具加载。命令行方式:
# 注册驱动服务 sc create TdiDriverSource type= kernel binPath= C:\path\to\TdiDriver.sys # 启动驱动 sc start TdiDriverSource # 查看状态 sc query TdiDriverSource # 停止并删除 sc stop TdiDriverSource sc delete TdiDriverSource加载成功后,用DebugView或者WinDbg看内核输出。代码里一般会在DriverEntry和回调函数里加KdPrint,你可以在DebugView里看到连接事件的打印。验证是否真正挂钩成功,可以在这台机器上发起一个TCP连接(比如浏览器访问一个地址),看DebugView里有没有对应的连接建立日志。如果没有,检查ZwCreateFile是否返回了STATUS_SUCCESS,以及事件回调是否真的被注册到了正确的设备上。
注意:测试驱动一定要在虚拟机里做,蓝屏了直接回滚快照。TDI驱动一旦在回调里出错,系统直接崩,没有后悔药。
4. 避坑与排查:TDI驱动开发的血泪经验
4.1 回调函数里等锁导致蓝屏
现象:驱动加载后,一有网络连接就蓝屏,错误码IRQL_NOT_LESS_OR_EQUAL。原因:TDI事件回调运行在DISPATCH_LEVEL,你如果在回调里调用KeWaitForSingleObject或者访问分页内存,直接触发页错误。解决:回调里只做最轻量的操作——拷贝数据到非分页池,然后IoQueueWorkItem丢到工作线程里处理。所有可能阻塞的操作都放到WORK_QUEUE里。
4.2 设备句柄泄漏导致无法卸载
现象:sc stop停不掉驱动,系统提示设备正在使用。原因:ZwCreateFile打开的TCP设备句柄没有在DriverUnload里ZwClose掉,引用计数不归零。解决:在DriverUnload里确保关闭所有打开的句柄,并且注销事件回调。顺序很重要:先注销回调,再关句柄,最后删设备对象。
4.3 IRP完成例程里重复释放
现象:随机蓝屏,错误码BAD_POOL_CALLER或MULTIPLE_IRP_COMPLETE_REQUESTS。原因:TDI的IRP是异步的,你在完成例程里释放了IRP,但底层驱动可能又碰了它。或者你对同一个IRP调了两次IoCompleteRequest。解决:IRP的所有权要清晰——谁分配谁释放,完成例程里只做状态检查,不碰IRP内存。用IoSetCompletionRoutine时注意InvokeOnSuccess、InvokeOnError、InvokeOnCancel三个标志都要设对。
4.4 绑定设备名写错导致STATUS_OBJECT_NAME_NOT_FOUND
现象:ZwCreateFile返回STATUS_OBJECT_NAME_NOT_FOUND,驱动加载失败。原因:设备名写成了\\Device\\TCP(大写)或者\\Device\\Tcp6(IPv6设备名不同)。解决:TDI的TCP设备名是\\Device\\Tcp,大小写敏感。IPv6是\\Device\\Tcp6,UDP是\\Device\\Udp。用RtlInitUnicodeString时确保字符串正确。
4.5 在DriverEntry里做太多事导致加载超时
现象:sc start卡住,最后报ERROR_SERVICE_REQUEST_TIMEOUT。原因:DriverEntry里做了耗时的同步操作,比如等待网络设备就绪。解决:DriverEntry只做最必要的初始化,把绑定TDI设备的操作放到一个独立的系统线程或者IoQueueWorkItem里异步做。DriverEntry返回STATUS_SUCCESS要快。
5. 进阶技巧:用TDI做连接审计与数据过滤
5.1 连接审计的实现思路
连接审计的核心是在ConnectionHandler回调里拿到五元组信息。TDI的连接事件会带一个TDI_CONNECTION_INFORMATION结构,里面包含远端地址和本地地址。代码里这样提取:
NTSTATUS TdiConnectionHandler( PVOID TdiEventContext, PTDI_CONNECTION_INFORMATION RemoteAddress, PTDI_CONNECTION_INFORMATION LocalAddress, ULONG Flags) { // 提取远端IP和端口 if (RemoteAddress->RemoteAddressLength >= sizeof(TA_IP_ADDRESS)) { PTA_IP_ADDRESS pAddr = (PTA_IP_ADDRESS)RemoteAddress->RemoteAddress; // pAddr->Address.in_addr 是IPv4地址 // pAddr->Port 是端口(网络字节序) ULONG ip = pAddr->Address.in_addr; USHORT port = pAddr->Port; // 记录到审计日志(非分页池) KdPrint(("TDI Connect: %d.%d.%d.%d:%d\n", (ip >> 0) & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF, ntohs(port))); } return STATUS_SUCCESS; }这段代码的关键点:TA_IP_ADDRESS是TDI定义的地址结构,Address.in_addr是IPv4地址(网络字节序),Port也是网络字节序,打印时要用ntohs转一下。RemoteAddressLength要检查,防止越界。审计日志用KdPrint只是演示,实际产品里应该写到环形缓冲区或者通过IOCTL上报给用户态。
5.2 数据过滤的边界与限制
TDI能在连接层面拦截,但数据过滤要看具体场景。ReceiveHandler回调能拿到接收到的数据,但TDI的数据包可能被分片,一个完整的应用层消息可能跨多个回调。如果你要做协议解析,得自己维护重组缓冲区。另外,TDI不提供发送方向的拦截——你只能看到收到的数据,发出去的数据要拦需要更底层的方案。
一个实用的技巧是:在ReceiveHandler里只做标记和计数,把数据拷贝到队列里,由工作线程做深度解析。这样避免在DISPATCH_LEVEL做复杂逻辑。队列用LIST_ENTRY加自旋锁保护,注意锁的粒度。
5.3 验证驱动是否真正生效
写完驱动,怎么确认它真的在干活?我一般用三步验证法:
| 验证步骤 | 操作 | 预期结果 |
|---|---|---|
| 加载验证 | sc start后查sc query | STATE为RUNNING |
| 事件验证 | 发起TCP连接,看DebugView | 有连接日志输出 |
| 数据验证 | 传输已知数据,检查计数 | 接收字节数与实际一致 |
如果事件验证没输出,先检查ZwCreateFile的返回值,再检查事件回调是否注册到了正确的设备。如果数据验证对不上,检查ReceiveHandler里的缓冲区长度处理——TDI可能把一个大包拆成多个回调,你的计数逻辑要累加而不是覆盖。
5.4 从TDI迁移到WFP的时机判断
TDI能用,但什么时候该换WFP?我的经验是:如果你的需求只是连接审计、简单的端口过滤,TDI够用且代码量小。但如果要做应用层协议识别、流量整形、和Windows过滤平台的其他组件联动,趁早转WFP。迁移的信号包括:需要拦截发送方向的数据、需要和用户态过滤引擎配合、需要支持Windows 10之后的新的网络特性。TDI的代码结构清晰,理解它的IRP和事件模型之后,转WFP的callout模型会快很多——底层思路是通的,只是框架换了套壳。
从那以后我每次写内核网络驱动,都强制自己在回调函数里只做三件事:拷贝、标记、丢队列。所有复杂逻辑放到工作线程,所有锁操作检查IRQL,所有IRP完成例程只读状态不碰内存。这三条习惯帮我省了至少十次蓝屏调试。希望帮到你。
本文还有配套的精品资源,点击获取