一、驱动入口与 NDIS 注册
在DriverEntry中,除了填充NDIS_MINIPORT_DRIVER_CHARACTERISTICS外,还特别注入了IRP_MJ_DEVICE_CONTROL、IRP_MJ_CLOSE、IRP_MJ_PNP的自定义处理函数。这些处理函数会在用户态调用DeviceIoControl或进程关闭文件句柄时被调用,用于注册/注销环形缓冲区及清理。
二、适配器初始化(TunInitializeEx)
当 NDIS 需要启动一个 Wintun 适配器实例时,调用此函数。
流程详解:
- 分配上下文:使用
ExAllocatePoolZero在非分页池分配TUN_CTX,并清零。 - 保存 NDIS 句柄:
Ctx->MiniportAdapterHandle = MiniportAdapterHandle。 - 获取设备对象:调用
NdisMGetDeviceProperty取得FunctionalDeviceObject,并将Ctx存入DeviceObject->Reserved,以便后续 IRP 分发使用。 - 初始化同步对象:
- 事件
Device.Disconnected(初始为 signaled,表示未连接)。 - 自旋锁
Device.Send.Lock和Device.Receive.Lock。 - 事件
Device.Receive.ActiveNbls.Empty(初始 signaled)。 - 资源锁
Device.RegistrationLock。
- 事件
- 分配 NBL 池:调用
NdisAllocateNetBufferListPool,指定fAllocateNetBuffer = TRUE,池标签为'wtun'。 - 设置 NDIS 属性:填充
NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES和NDIS_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES:- 介质类型:
NdisMediumIP(直接处理 IP 包,无以太网头)。 - MTU:
0xFFFF(最大 IP 包大小)。 - 链路速度:100 Gbps(虚拟,实际受用户态处理限制)。
- 支持 OID 列表:包括统计、电源管理、链路参数等。
- 电源管理能力:不支持唤醒。
- 标志:允许意外移除。
- 介质类型:
- 通过
NdisMSetMiniportAttributes两次调用分别注册这两组属性。 - 返回
NDIS_STATUS_SUCCESS。此时适配器处于“已停止”状态,Running = 0(初始值)。
三、暂停与重启
3.1TunPause
当 NDIS 要求暂停(例如系统进入低功耗状态或正在卸载驱动)时调用:
- 将
Running置为 0(使用WriteRelease)。 - 获取
TransitionLock的独占锁并立即释放,使所有正在进行的读路径(持有共享锁)能感知到Running的变化。 - 等待
Device.Receive.ActiveNbls.Empty事件,确保所有已指示的接收 NBL 都已返回(避免使用已释放的 MDL)。 - 返回
NDIS_STATUS_SUCCESS。
3.2TunRestart
恢复运行时调用:
- 将
Running置为 1(WriteRelease)。 - 返回成功。
四、适配器停止(TunHaltEx)
当适配器被永久移除(如用户手动删除设备)时调用:
- 调用
TunUnregisterBuffers强制注销(TUN_FORCE_UNREGISTRATION),断开连接、释放资源。 - 再次获取
TransitionLock独占锁,确保所有读者已退出。 - 释放 NBL 池。
- 清空
MiniportAdapterHandle和FunctionalDeviceObject->Reserved。 - 删除资源锁
Device.RegistrationLock。 - 释放
TUN_CTX内存。
五、缓冲区的注册与注销
5.1 注册(TunRegisterBuffers)
由用户态DeviceIoControl(TUN_IOCTL_REGISTER_RINGS)触发,该 IOCTL 最终会调用到此函数。
核心步骤:
- 获取
Device.RegistrationLock独占锁,检查是否已有OwningFileObject,若有则返回STATUS_ALREADY_INITIALIZED。 - 从 IRP 的输入缓冲区解析
TUN_REGISTER_RINGS结构(支持 32/64 位跨进程)。 - 校验参数:
- 计算
Capacity = TUN_RING_CAPACITY(RingSize),必须介于 128 KiB 和 64 MiB 之间,且为 2 的幂。 - 检查事件句柄(
TailMoved)是否有效,并通过ObReferenceObjectByHandle获取内核事件对象指针(用于驱动唤醒)。
- 计算
- 锁定用户内存:
- 分配 MDL,调用
MmProbeAndLockPages将用户空间环形缓冲区锁定在物理内存中。 - 使用
MmGetSystemAddressForMdlSafe获取内核地址空间映射(NonPagedPool,且带MdlMappingNoExecute)。
- 分配 MDL,调用
- 读取初始 Tail 指针:从用户环中读取当前 Tail 并保存到
Ctx->Device.Send.RingTail。 - 创建接收线程:调用
PsCreateSystemThread创建内核线程TunProcessReceiveData,负责从接收环读取数据并转发给 NDIS。 - 保存进程 ID:
Ctx->Device.OwningProcessId = PsGetCurrentProcessId()。 - 加入全局设备列表:获取
TunDispatchDeviceListLock,将Ctx->Device.Entry插入链表(用于后续孤儿检测)。 - 指示链接状态为已连接:调用
TunIndicateStatus(MediaConnectStateConnected)。 - 释放
RegistrationLock,返回成功。
5.2 注销(TunUnregisterBuffers)
由WintunEndSession或进程退出、设备移除等触发:
- 获取
RegistrationLock独占锁,确认OwningFileObject与调用者匹配(若为强制注销,忽略匹配)。 - 从全局设备列表中移除自身。
- 指示链接状态为断开。
- 置位
Device.Disconnected事件,并获取TransitionLock独占锁,使所有读路径感知到断开。 - 等待接收线程退出(通过
ObReferenceObjectByHandle获取线程对象,KeWaitForSingleObject等待)。 - 关闭接收线程句柄。
- 将发送环的
Tail设为MAXULONG,并触发Send.TailMoved事件,让可能的等待者退出。 - 解锁用户内存(
MmUnlockPages)、释放 MDL、取消引用事件对象。 - 清空
OwningFileObject,释放锁。
六、进程退出通知(TunProcessNotification)
注册了PsSetCreateProcessNotifyRoutine,在进程退出时被回调:
- 如果
Create == FALSE(进程退出),则遍历全局设备列表,查找OwningProcessId == ProcessId的设备。 - 若找到,则调用
TunUnregisterBuffers(TUN_FORCE_UNREGISTRATION)强制断开该进程持有的适配器,避免资源泄漏。
七、设备对象的 IRP 处理
7.1TunDispatchDeviceControl
拦截IRP_MJ_DEVICE_CONTROL,仅处理TUN_IOCTL_REGISTER_RINGS,其他交给 NDIS 默认处理。
- 首先进行安全访问检查(
SeAccessCheck),确保调用者有FILE_WRITE_DATA权限。 - 获取
TunDispatchCtxGuard共享锁(保护DeviceObject->Reserved),调用TunRegisterBuffers。
7.2TunDispatchClose
拦截IRP_MJ_CLOSE,当用户态关闭设备句柄时触发:
- 获取
TunDispatchCtxGuard共享锁,调用TunUnregisterBuffers并传入当前FileObject。 - 将原关闭处理函数(
NdisDispatchClose)的结果返回。
7.3TunDispatchPnp
拦截IRP_MJ_PNP,主要处理IRP_MN_QUERY_REMOVE_DEVICE和IRP_MN_SURPRISE_REMOVAL。
在此函数中,驱动会尝试强制关闭仍持有设备句柄的其他进程。具体做法:
- 获取
RegistrationLock。 - 如果当前存在
OwningFileObject且不是本文件对象,则枚举系统句柄表(ZwQuerySystemInformation),查找与OwningFileObject相同的对象句柄,并关闭它们(通过ObCloseHandle)。 - 这样做的目的是防止其他进程持有句柄导致设备无法移除。
- 最终交给
NdisDispatchPnp完成剩余 PnP 处理。
八、总结
本文档详细描述了驱动的初始化、暂停、重启、关闭、缓冲区注册与注销,以及进程退出通知和 IRP 拦截。这些机制共同保证了适配器的生命周期管理、资源安全和跨进程共享的正确性,特别是通过进程通知和句柄枚举,有效避免了“僵尸适配器”问题。下一篇文章将聚焦最关键的数据路径——发送与接收。