第4篇:NDIS 驱动是什么鬼 —— Windows 网络栈的地下室

第4篇:NDIS 驱动是什么鬼 ------ Windows 网络栈的地下室

一、下潜到网络栈的"地下室"

大部分人一辈子只跟应用层打交道------socket()connect()send()。这就像你住在一栋大楼的一楼大堂,宽敞明亮,有空调有沙发。你完全不需要知道脚下的地下室里正在发生什么。

但如果你想要拦截------不是"看"一个包,是"劫持"一个包------那你必须下地下室。那里又黑又冷,管道交错,空气中有一股潮湿的混凝土味道。但所有的管道都从那里经过。

Windows 的这个地下室,叫 NDIS。

二、什么是 NDIS?它的位置在哪里?

NDIS 全称 Network Driver Interface Specification。名字很朴素------"网络驱动程序接口规范"。它在 Windows 网络栈里的位置,简单说是这样的:

复制代码
┌──────────────────────────┐
│   你的程序 (socket)       │  ← "我要上 google.com!"
├──────────────────────────┤
│   TCP/IP 协议栈          │  ← "我知道怎么封装 TCP 包"
│   (tcpip.sys)            │
├──────────────────────────┤
│        NDIS              │  ← ★ 我们站在这里
├──────────────────────────┤
│   网卡驱动 (e1d.sys等)    │  ← "我会跟网卡芯片说悄悄话"
├──────────────────────────┤
│   网卡硬件               │  ← 电线 / WiFi 天线
└──────────────────────────┘

NDIS 在中间。对上跟协议栈打交道("你要发包?给我。"),对下跟网卡驱动打交道("网线上来了数据?给我看看。")。所有进出这台机器的网络流量,都必须经过它。

这就像一个城市的供水系统。你家水龙头是应用层(一拧就出水),自来水厂是网卡硬件(水源),而 NDIS 是埋在地下的主供水管道。你要在水里加氟、过滤、或者------如果你有恶意的话------下毒,最好的地方是主供水管道。在水龙头那里加,只能影响一家一户;在主管道加,影响整座城。

三、NDIS 的三驾马车:Miniport、Protocol 与 Filter

NDIS 内部有三种角色,构成了一个三层金字塔:

Miniport Driver(微型端口驱动):最底层,管硬件的。每个网卡厂商都要写一个。Intel 的网卡有 Intel 的 Miniport,Realtek 有 Realtek 的。它知道怎么跟特定的网卡芯片对话------哪个寄存器是写 MAC 地址的,哪个中断是"数据到了"------但它不知道 IP 和 TCP 是什么意思。它的世界观止步于"以太网帧"。

Protocol Driver(协议驱动):最上层,管协议的。TCP/IP 协议栈本身就是 Protocol Driver。它的世界观是 IP 地址、端口号、序列号。它不知道也不关心网卡是 Intel 的还是 Realtek 的。

Filter Driver(过滤驱动):夹在中间,管拦截的。它是 NDIS 6.0(Vista 之后)才引入的角色。一个 Filter Driver 可以插入在 Miniport 和 Protocol 之间------对于从网线进来的包,先经过 Filter,再到 Protocol;对于发出去的包,先经过 Protocol,再到 Filter,再到网线。

WinPkFilter 是 Filter Driver 和 Protocol Driver 的合体。这种双重身份让它极其强大------既能在包到达协议栈之前拦截(Filter 的能力),又能自己作为一个"假协议栈"接收包(Protocol 的能力)。

四、搬运数据包的容器:INTERMEDIATE_BUFFER

在 WinPkFilter 的世界里,每个被抓到的数据包都住在一个叫 INTERMEDIATE_BUFFER 的结构体里。这个结构体是整个系统最核心的数据结构。你不用记住它的每一个字段,但要知道它的存在和大概形态:

前面一坨是元数据------这个包来自哪个网卡、是进还是出、多长。最后有一个大数组 m_IBuffer[1514],里面装着完整的以太网帧------从目的 MAC 地址开始,一直到最后的 CRC 校验(虽然通常已被硬件剥离)。

1514 这个数字怎么来的?标准以太网 MTU = 1500 字节 + 14 字节帧头 = 1514。如果开启 Jumbo Frame,MTU 可以到 9000,帧总长就是 9014。

这就是我们在整个系统中搬运数据包的基本单位。不是"一个 IP 包",不是"一个 TCP 段"------是一个完整的以太网帧。

五、用户态与内核态的通信桥梁:IOCTL

用户态程序(我们的 exe)和内核态驱动(WinPkFilter)之间的通信方式叫 IOCTL(I/O Control)。它的流程就像写信:

  1. 用户态打开设备文件 \\.\NDISRD(就像拿到信纸)
  2. 调用 `DeviceIoControl(句柄, 操作码, 输入, 输出, ...)」(写信、贴邮票、寄出)
  3. Windows 内核把请求转发给驱动的 IRP 处理函数(邮局分拣)
  4. 驱动处理完,结果拷回用户态的 output buffer(回信到达)
  5. DeviceIoControl 返回(你拆开信封,读回信)

WinPkFilter 定义了几十个操作码,但最常用的是这五个:

操作码 干什么
GET_TCPIP_INTERFACES 列出系统上绑了 TCP/IP 的网卡
SET_ADAPTER_MODE 设置网卡工作模式
READ_PACKETS 批量读取拦截到的包
SEND_TO_MSTCP 注入一个包到协议栈("这是从网线来的")
SEND_TO_ADAPTER 注入一个包到网卡("这是要发出去的")

注意 READ_PACKETS 是复数------批量操作。一次 IOCTL 调用要花几百个 CPU 周期(主要是上下文切换)。如果你一个一个读包,每个包都付这笔税。一次读 512 个,平摊下来每个包的成本几乎为零。

六、内核对齐与防蓝屏的生存之道

内核态和用户态共享同一套结构体定义------INTERMEDIATE_BUFFER 在内核驱动里的含义和用户态程序里完全一致。如果两边对结构体布局的理解差了一个字节------比如某个字段的对齐方式不一样------数据就全乱了。而内核态数据乱了的结果,通常是蓝屏。

这就是为什么 Common.h 有 1000+ 行。它用 #pragma pack(push,1) 强制 1 字节对齐,用大量的 typedef 确保 32 位和 64 位系统布局一致。它甚至定义了每个结构体的 WOW64 版本(比如 INTERMEDIATE_BUFFER_WOW64),因为 32 位程序在 64 位系统上运行时,指针大小不一样。

这些看起来啰嗦的设计,背后是驱动开发者被蓝屏折磨出来的血泪史。

七、预告:亲手"偷"出第一个数据包

下一篇,我们会真的从驱动里读出一个包。不是看结构体定义,不是看 MSDN 文档------是让代码跑起来,从网卡上偷一个包下来,然后逐字节翻译成人话。这大概是整个系列里最让人兴奋的一篇,因为你第一次能亲眼看到网络上流动的数据长什么样。


本文是《从0到1编写一个硬核软路由》系列的第四篇。上一篇:第3篇:全景架构图 | 下一篇:第5篇:第一次偷包