Windows驱动开发入门-NDIS协议驱动

碎碎念

本节代码需要最低警告、关警告按错误处理、关符合模式、关SDL检查、导入ndis.lib和wdmsec.lib。可直接参考Github上微软官方的驱动代码示例库的ndisprot工程。本节代码用传统型驱动编码方式,后面NDIS小端口驱动将用WDF驱动编码方式。

最常用的Ethernet V2(ARPA)以太网包如下,若类型2字节为0x80、0x00则表示为IP包。

低地址 高地址
源网卡MAC地址 目标网卡MAC地址 类型 数据
6字节 6字节 2字节 其他

协议驱动接收上层用户的Socket请求,把这些数据封装为IP包,再把IP包封装成以太网包发送出去;接收到以太网包时,分析这时给哪个用户程序的,把用户数据解析出来,提交给上层应用程序。协议驱动较多用于嗅探,如Wincap,一般不用于防火墙,应为这玩意儿不好干预应用程序发送或接收包。

网卡驱动接口标准NDIS是一组定义好的函数接口的集合。NDIS网络驱动有三种:协议驱动、小端口驱动、中间层驱动(包含过滤驱动),开发者可提供这三种不同的内核模块给NDIS使用。协议驱动上层提供直接供应用层Socket使用的数据传输接口,下层绑定小端口,用于发送与接收以太网包。小端口驱动针对网卡,给协议层提供接收和发送数据包的能力。传统中间层驱动即将被过滤驱动淘汰,在协议驱动和小端口驱动之间。

开发历程为:填写协议特征(协议回调函数列表);把自己注册为协议驱动;系统对每个实际存在的网卡实例调用本协议驱动在协议特征集中提供的一个回调函数,在该回调函数中决定是否要绑定一个网卡,一旦绑定,该网卡接收到的包将提交给该协议驱动,后者也可用该网卡发送包;发生各种事件时,如网卡接收到一个新数据包,特征集中某个函数被调用,后者决定如何处理接收到的数据包;当应用层试图发送一个以太网包时,可打开该协议并发出请求。

协议驱动入口

要做的有:生成一个控制设备和一个符号链接,指定分发函数;注册一个协议,提供协议特征。入口在ntdisp.c中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
NTSTATUS DriverEntry(IN PDRIVER_OBJECT   pDriverObject, IN PUNICODE_STRING  pRegistryPath)
/*++
Routine Description:
Called on loading. We create a device object to handle user-mode requests on, and register ourselves as a protocol with NDIS.
Arguments:
pDriverObject - Pointer to driver object created by system.
pRegistryPath - Pointer to the Unicode name of the registry path for this driver.
Return Value:
NT Status code
--*/
{
NDIS_PROTOCOL_CHARACTERISTICS protocolChar; //协议特征
NTSTATUS status = STATUS_SUCCESS; //状态变量 返回值
NDIS_STRING protoName = NDIS_STRING_CONST("NdisProt");
UNICODE_STRING ntDeviceName;
UNICODE_STRING win32DeviceName;
BOOLEAN fSymbolicLink = FALSE;
PDEVICE_OBJECT deviceObject = NULL;
UNREFERENCED_PARAMETER(pRegistryPath);
DEBUGP(DL_LOUD, ("DriverEntry\n"));
Globals.pDriverObject = pDriverObject;
NPROT_INIT_EVENT(&Globals.BindsComplete);
do { //这里do-while当作try来用 出错用break快速退出
// Create our device object using which an application can access NDIS devices.
RtlInitUnicodeString(&ntDeviceName, NT_DEVICE_NAME); //初始化控制设备名
#ifndef WIN9X
status = IoCreateDeviceSecure(pDriverObject, 0, &ntDeviceName, FILE_DEVICE_NETWORK, FILE_DEVICE_SECURE_OPEN, FALSE, &SDDL_DEVOBJ_SYS_ALL_ADM_ALL, NULL, &deviceObject); //生成设备
#else
status = IoCreateDevice(pDriverObject, 0, &ntDeviceName, FILE_DEVICE_NETWORK, FILE_DEVICE_SECURE_OPEN, FALSE, &deviceObject);
#endif
if (!NT_SUCCESS(status))
// Either not enough memory to create a deviceobject or another deviceobject with the same name exits. This could happen if you install another instance of this device.
break;
RtlInitUnicodeString(&win32DeviceName, DOS_DEVICE_NAME);
status = IoCreateSymbolicLink(&win32DeviceName, &ntDeviceName); //生成符号链接
if (!NT_SUCCESS(status))
break;
fSymbolicLink = TRUE;
deviceObject->Flags |= DO_DIRECT_IO; //直接IO方式
Globals.ControlDeviceObject = deviceObject; //控制设备指针
NPROT_INIT_LIST_HEAD(&Globals.OpenList); //初始化链表
NPROT_INIT_LOCK(&Globals.GlobalLock); //初始化一个锁
// Initialize the protocol characterstic structure
NdisZeroMemory(&protocolChar, sizeof(NDIS_PROTOCOL_CHARACTERISTICS)); //填写协议特征
protocolChar.MajorNdisVersion = 5;
protocolChar.MinorNdisVersion = 0;
protocolChar.Name = protoName;
protocolChar.OpenAdapterCompleteHandler = NdisProtOpenAdapterComplete;
protocolChar.CloseAdapterCompleteHandler = NdisProtCloseAdapterComplete; //关闭完成回调
protocolChar.SendCompleteHandler = NdisProtSendComplete;
protocolChar.TransferDataCompleteHandler = NdisProtTransferDataComplete;
protocolChar.ResetCompleteHandler = NdisProtResetComplete;
protocolChar.RequestCompleteHandler = NdisProtRequestComplete; //请求发送后未决 完成时调用该完成函数
protocolChar.ReceiveHandler = NdisProtReceive;
protocolChar.ReceiveCompleteHandler = NdisProtReceiveComplete;
protocolChar.StatusHandler = NdisProtStatus;
protocolChar.StatusCompleteHandler = NdisProtStatusComplete;
protocolChar.BindAdapterHandler = NdisProtBindAdapter; //内核检测到网卡存在时 调用每个注册过的协议的该回调函数
protocolChar.UnbindAdapterHandler = NdisProtUnbindAdapter; //解绑回调
protocolChar.UnloadHandler = NULL;
protocolChar.ReceivePacketHandler = NdisProtReceivePacket;
protocolChar.PnPEventHandler = NdisProtPnPEventHandler;
// Register as a protocol driver
NdisRegisterProtocol((PNDIS_STATUS)&status, &Globals.NdisProtocolHandle, &protocolChar, sizeof(NDIS_PROTOCOL_CHARACTERISTICS)); //注册协议
if (status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("Failed to register protocol with NDIS\n"));
status = STATUS_UNSUCCESSFUL;
break;
};
#ifdef NDIS51
Globals.PartialCancelId = NdisGeneratePartialCancelId();
Globals.PartialCancelId <<= ((sizeof(PVOID) - 1) * 8);
DEBUGP(DL_LOUD, ("DriverEntry: CancelId %lx\n", Globals.PartialCancelId));
#endif
// Now set only the dispatch points we would like to handle.
pDriverObject->MajorFunction[IRP_MJ_CREATE] = NdisProtOpen; //填写分发函数
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = NdisProtClose;
pDriverObject->MajorFunction[IRP_MJ_READ] = NdisProtRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = NdisProtWrite;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = NdisProtCleanup;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = NdisProtIoControl;
pDriverObject->DriverUnload = NdisProtUnload;
status = STATUS_SUCCESS;
} while (FALSE);
if (!NT_SUCCESS(status)) { //失败则释放资源
if (deviceObject) {
IoDeleteDevice(deviceObject);
Globals.ControlDeviceObject = NULL;
};
if (fSymbolicLink)
IoDeleteSymbolicLink(&win32DeviceName);
};
return status;
};

协议与网卡绑定

当一个协议驱动绑定了一个网卡时,网卡收到的数据包会提交给该协议,协议可用该网卡发送数据包。网卡也可以是虚拟网卡。本小节详细讲NdisProtBindAdapter内部工作原理,想直接开发的就跳过本小节。

协议驱动的打开上下文:打开指的是打开网卡,也就是绑定网卡,NDIS中用NdisOpenAdapter。该驱动中每当有一个网卡被绑定,就分配一个内存空间来保存和这次绑定相关的信息,以及本次绑定所需要用到的资源,如锁和队列等。

NdisProtBindAdapter工作有:打开上下文的分配和初始化;将打开的上下文保存到全局链表中,完成绑定。代码在ndisbind.c中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
VOID NdisProtBindAdapter(OUT PNDIS_STATUS pStatus, IN NDIS_HANDLE BindContext, IN PNDIS_STRING pDeviceName, IN PVOID SystemSpecific1, IN PVOID SystemSpecific2)
/*++
Routine Description:
Protocol Bind Handler entry point called when NDIS wants us to bind to an adapter. We go ahead and set up a binding. An OPEN_CONTEXT structure is allocated to keep state about this binding.
Arguments:
pStatus - place to return bind status
BindContext - handle to use with NdisCompleteBindAdapter
DeviceName - adapter to bind to
SystemSpecific1 - used to access protocol-specific registry key for this binding
SystemSpecific2 - unused
Return Value:
None
--*/
{
PNDISPROT_OPEN_CONTEXT pOpenContext;
NDIS_STATUS Status, ConfigStatus;
NDIS_HANDLE ConfigHandle;
UNREFERENCED_PARAMETER(BindContext);
UNREFERENCED_PARAMETER(SystemSpecific2);
do {
// Allocate our context for this open.
// 分配空间给每个打开上下文。所谓打开上下文就是每次绑定,用户分配的一片空间,用来保存这次绑定相关的信息。这里用宏NPROT_ALLOC_MEM分配内存是为了调试的方便。实际上本质是用NdisAllocateMemoryWithTag分配空间。读者如果用ExAllocatePoolWithTag代替也是可行的。只是要注意必须是Nonpaged空间。
NPROT_ALLOC_MEM(pOpenContext, sizeof(NDISPROT_OPEN_CONTEXT));
if (pOpenContext == NULL) {
Status = NDIS_STATUS_RESOURCES;
break;
};
// 内存清0。同样用宏。实际上用的NdisZeroMemory。
NPROT_ZERO_MEM(pOpenContext, sizeof(NDISPROT_OPEN_CONTEXT));
// 给这个空间写一个特征数据便于识别判错。
NPROT_SET_SIGNATURE(pOpenContext, oc);
// 初始化几个用到的数据成员。锁、读队列、写对队列、包队列
// 电源打开事件
NPROT_INIT_LOCK(&pOpenContext->Lock);
NPROT_INIT_LIST_HEAD(&pOpenContext->PendedReads);
NPROT_INIT_LIST_HEAD(&pOpenContext->PendedWrites);
NPROT_INIT_LIST_HEAD(&pOpenContext->RecvPktQueue);
NPROT_INIT_EVENT(&pOpenContext->PoweredUpEvent);
// Start off by assuming that the device below is powered up.
// 认为开始的时候电源是打开的。
NPROT_SIGNAL_EVENT(&pOpenContext->PoweredUpEvent);
// Determine the platform we are running on.
// 下面开始检测我们运行在什么平台。首先假定是Win9x.但是为了去掉多余的部分,实际上我已经去掉了对Win9x的支持。所以下面这一段已经没有意义了。但是下面的代码依然有参考价值。实际上是在读取注册表的配置。
//pOpenContext->bRunningOnWin9x = TRUE;
//NdisOpenProtocolConfiguration(&ConfigStatus,&ConfigHandle,(PNDIS_STRING)SystemSpecific1);
//if (ConfigStatus == NDIS_STATUS_SUCCESS){
// PNDIS_CONFIGURATION_PARAMETER pParameter;
// NDIS_STRING VersionKey = NDIS_STRING_CONST("Environment");
// NdisReadConfiguration(&ConfigStatus,&pParameter,ConfigHandle,&VersionKey,NdisParameterInteger);
// if ((ConfigStatus == NDIS_STATUS_SUCCESS) && ((pParameter->ParameterType == NdisParameterInteger) || (pParameter->ParameterType == NdisParameterHexInteger)))
// pOpenContext->bRunningOnWin9x = (pParameter->ParameterData.IntegerData == NdisEnvironmentWindows);
// NdisCloseConfiguration(ConfigHandle);
//};
NPROT_REF_OPEN(pOpenContext);
// Add it to the global list.
// 因为打开上下文已经被分配好。所以这里将这个打开上下文保存到全局链表里以便日后检索。注意这个操作要加锁。实际上这里用的就是读者前面学过的自旋锁。
NPROT_ACQUIRE_LOCK(&Globals.GlobalLock);
NPROT_INSERT_TAIL_LIST(&Globals.OpenList, &pOpenContext->Link);
NPROT_RELEASE_LOCK(&Globals.GlobalLock);
// 正式的绑定过程。
Status = ndisprotCreateBinding(pOpenContext, (PUCHAR)pDeviceName->Buffer, pDeviceName->Length);
if (Status != NDIS_STATUS_SUCCESS)
break;
} while (FALSE);
*pStatus = Status;
return;
};

协议绑定网卡用NdisOpenAdapter,原型如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
VOID NdisOpenAdapter(
_Out_ PNDIS_STATUS Status, //状态
_Out_ PNDIS_STATUS OpenErrorStatus, //错误码
_Out_ PNDIS_HANDLE NdisBindingHandle, //绑定句柄
_Out_ PUINT SelectedMediumIndex, //最终选择的媒质类型数组下标
_In_ PNDIS_MEDIUM MediumArrary, //该协议支持的所有媒质类型 本章内容只有NdisMedium802_3
_In_ UINT MediumArraySize, //媒质数组大小
_In_ NDIS_HANDLE NdisProtocolHandle, //协议句柄
_In_ NDIS_HANDLE ProtocolBindingContext, //上下文指针
_In_ PNDIS_STRING AdapterName, //要绑定的网卡名
_In_ UINT OpenOptions, //0
_In_opt_ PSTRING AddressingInformation //用来编码网卡的地址 一般NULL
);

ndisprotCreateBinding工作有:防止多线程竞争;分配和初始化本次绑定的相关资源;获得网卡参数,代码如下。

自旋锁会提高中断级别,在此之后很多事情就不能做了。自旋锁非常消耗资源,在锁住时会不断重试,此时不宜做耗时长、复杂的操作。

需要分配的资源有输入和输出缓冲区的包池和缓冲池。包池是一组预先分配好的包描述符,缓冲池是一组已分配好的包缓冲区描述符。每个以太网包都用一个NDIS_PACKET包描述符来描述,实际内容包含在NDIS_BUFFER包缓冲区描述符。网卡接收到包后要保存到报池中等待上层应用程序取走,上层应用程序要求发送的包先保存在包池中再发送出去。这样在具体有以太网包需要存入时,不用再次分配包描述符和包缓冲区描述符,以此减少动态分配内存的消耗。

NDIS对象描述符OID是一组系统定义的常熟,每个OID为一个请求的类型号。NDIS上层驱动可以给下层驱动发送OID请求,下层驱动必需返回约定的结果。如可以获取网卡MAC地址、最大帧长等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
NDIS_STATUS ndisprotCreateBinding(IN PNDISPROT_OPEN_CONTEXT pOpenContext,__in_bcount(BindingInfoLength) IN PUCHAR pBindingInfo,IN ULONG BindingInfoLength) {
NDIS_STATUS Status;
NDIS_STATUS OpenErrorCode;
NDIS_MEDIUM MediumArray[1] = { NdisMedium802_3 };
UINT SelectedMediumIndex;
PNDISPROT_OPEN_CONTEXT pTmpOpenContext;
BOOLEAN fDoNotDisturb = FALSE;
BOOLEAN fOpenComplete = FALSE;
ULONG BytesProcessed;
ULONG GenericUlong = 0;
// 输出一句调试信息。
DEBUGP(DL_LOUD, ("CreateBinding: open %p/%x, device [%ws]\n",pOpenContext, pOpenContext->Flags, pBindingInfo));
Status = NDIS_STATUS_SUCCESS;
do {
// 检查看看是否已经绑定了这个网卡。如果已经绑定的话,就没有必要再次绑定了,直接返回成功即可。请注意,ndisprotLookupDevice会给这个打开上下文增加一个引用。
pTmpOpenContext = ndisprotLookupDevice(pBindingInfo, BindingInfoLength);
// 如果没有找到的话,就返回NULL了。
if (pTmpOpenContext != NULL) {
DEBUGP(DL_WARN, ("CreateBinding: Binding to device %ws already exists on open %p\n", pTmpOpenContext->DeviceName.Buffer, pTmpOpenContext));
// 减少这个打开上下文的一个引用。
NPROT_DEREF_OPEN(pTmpOpenContext);
Status = NDIS_STATUS_FAILURE;
break;
};
// 获得锁。为什么这里要用锁?这是因为只有对空闲的打开上下文
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 通过标记来检查...如果绑定标志不是空闲状态,或者解除绑定信息收到了解除绑定的要求,那就直接返回失败。 NUIOO_BIND_IDLE当pOpenContext有绑定已完成、或正进行绑定、或有其他操作时不为0
if (!NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_IDLE) || NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_UNBIND_FLAGS, NUIOO_UNBIND_RECEIVED)) {
NPROT_RELEASE_LOCK(&pOpenContext->Lock); //释放锁
Status = NDIS_STATUS_NOT_ACCEPTED;
// 设置了标记,表示
fDoNotDisturb = TRUE;
break;
};
// 设置标记,表示我们已经开始绑定了。别人勿操作它。
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_OPENING);
// 释放锁。
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 以下开始绑定 不用担心其他线程进入这里
// 分配名字。到这里开始绑定了。先分配设备名字符串。
NPROT_ALLOC_MEM(pOpenContext->DeviceName.Buffer, BindingInfoLength + sizeof(WCHAR));
if (pOpenContext->DeviceName.Buffer == NULL) {
DEBUGP(DL_WARN, ("CreateBinding: failed to alloc device name buf (%d bytes)\n", BindingInfoLength + sizeof(WCHAR)));
Status = NDIS_STATUS_RESOURCES;
break;
};
// 从pBindingInfo中把字符串拷贝出来。
NPROT_COPY_MEM(pOpenContext->DeviceName.Buffer, pBindingInfo, BindingInfoLength);
#pragma prefast(suppress: 12009, "DeviceName length will not cause overflow")
* (PWCHAR)((PUCHAR)pOpenContext->DeviceName.Buffer + BindingInfoLength) = L'\0';
NdisInitUnicodeString(&pOpenContext->DeviceName, pOpenContext->DeviceName.Buffer);
// 分配包池。用来做发送缓冲区,容纳将要发送出去的包。
NdisAllocatePacketPoolEx(&Status,&pOpenContext->SendPacketPool,MIN_SEND_PACKET_POOL_SIZE,MAX_SEND_PACKET_POOL_SIZE - MIN_SEND_PACKET_POOL_SIZE,sizeof(NPROT_SEND_PACKET_RSVD)); //pOpenContext->SendPacketPool为发送缓冲区包池
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: failed to alloc send packet pool: %x\n", Status));
break;
};
// 分配包池,用来容纳
NdisAllocatePacketPoolEx(&Status,&pOpenContext->RecvPacketPool,MIN_RECV_PACKET_POOL_SIZE,MAX_RECV_PACKET_POOL_SIZE - MIN_RECV_PACKET_POOL_SIZE,sizeof(NPROT_RECV_PACKET_RSVD));
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: failed to alloc recv packet pool: %x\n", Status));
break;
};
// 包池。用来做接收缓冲区。
NdisAllocateBufferPool(&Status,&pOpenContext->RecvBufferPool,MAX_RECV_PACKET_POOL_SIZE);
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: failed to alloc recv buffer pool: %x\n", Status));
break;
};
// 电源状态是打开着的。
pOpenContext->PowerState = NetDeviceStateD0;
// 初始化一个打开事件。(打开就是绑定!)
NPROT_INIT_EVENT(&pOpenContext->BindEvent);
NdisOpenAdapter(&Status,&OpenErrorCode,&pOpenContext->BindingHandle,&SelectedMediumIndex,&MediumArray[0],sizeof(MediumArray) / sizeof(NDIS_MEDIUM),Globals.NdisProtocolHandle,(NDIS_HANDLE)pOpenContext,&pOpenContext->DeviceName,0,NULL);
// 等待请求完成。
if (Status == NDIS_STATUS_PENDING) {
NPROT_WAIT_EVENT(&pOpenContext->BindEvent, 0);
Status = pOpenContext->BindStatus;
};
// 如果不成功
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: NdisOpenAdapter (%ws) failed: %x\n", pOpenContext->DeviceName.Buffer, Status));
break;
};
// 记住我们已经成功的绑定了。但是我们还没有更新打开状态。这是为了避免别的线程开始关闭这个绑定。
fOpenComplete = TRUE;
// 发请求,获得一个可阅读的名字。不过这并不是非成功不可的。所以不检查返回值。
(VOID)NdisQueryAdapterInstanceName(&pOpenContext->DeviceDescr,pOpenContext->BindingHandle);
// 获得下面网卡的Mac地址
Status = ndisprotDoRequest(pOpenContext,NdisRequestQueryInformation,OID_802_3_CURRENT_ADDRESS,&pOpenContext->CurrentAddress[0],NPROT_MAC_ADDR_LEN,&BytesProcessed);
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: qry current address failed: %x\n", Status));
break;
};
// 获得网卡选项
Status = ndisprotDoRequest(pOpenContext,NdisRequestQueryInformation,OID_GEN_MAC_OPTIONS,&pOpenContext->MacOptions,sizeof(pOpenContext->MacOptions),&BytesProcessed);
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: qry MAC options failed: %x\n", Status));
break;
};
// 获得最大帧长
Status = ndisprotDoRequest(pOpenContext,NdisRequestQueryInformation,OID_GEN_MAXIMUM_FRAME_SIZE,&pOpenContext->MaxFrameSize,sizeof(pOpenContext->MaxFrameSize),&BytesProcessed);
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: qry max frame failed: %x\n", Status));
break;
};
// 获得下层连接状态。
Status = ndisprotDoRequest(pOpenContext,NdisRequestQueryInformation,OID_GEN_MEDIA_CONNECT_STATUS,&GenericUlong,sizeof(GenericUlong),&BytesProcessed);
if (Status != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("CreateBinding: qry media connect status failed: %x\n", Status));
break;
};
if (GenericUlong == NdisMediaStateConnected)
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_MEDIA_FLAGS, NUIOO_MEDIA_CONNECTED);
else
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_MEDIA_FLAGS, NUIOO_MEDIA_DISCONNECTED);
// 设置标记
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_ACTIVE);
// 检测是否这时候出现了一个解除绑定请求
if (NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_UNBIND_FLAGS, NUIOO_UNBIND_RECEIVED))
// 出现了则这次绑定失败
Status = NDIS_STATUS_FAILURE;
// 标记测试完之后就可以解锁了。
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
} while (FALSE);
// 如果没有成功,而且fDoNotDisturb为FALSE
if ((Status != NDIS_STATUS_SUCCESS) && !fDoNotDisturb) {
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 如果是已经成功的绑定了
if (fOpenComplete)
// 如果已经绑定结束了,设置已经绑定标记。
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_ACTIVE);
else if (NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_OPENING))
// 如果是正在绑定过程中,设置绑定失败了。
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_FAILED);
// 释放锁。
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 调用停止绑定函数。这里会释放所有资源。
ndisprotShutdownBinding(pOpenContext);
};
DEBUGP(DL_INFO, ("CreateBinding: OpenContext %p, Status %x\n",pOpenContext, Status));
return (Status);
};

上述ndisprotDoRequest函数封装了NdisRequest,所有OID都用它发送,原型如下:

1
2
3
4
5
VOID NdisRequest(
_Out_ PNDIS_STATUS Status, //返回结果
_In_ NDIS_HANDLE NdisBindingHandle, //绑定句柄
_In_ PNDIS_REQUEST NdisRequest //请求句柄
);

至于请求句柄咋填:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
NDIS_STATUS ndisprotDoRequest(IN PNDISPROT_OPEN_CONTEXT pOpenContext, IN NDIS_REQUEST_TYPE RequestType, IN NDIS_OID Oid, IN PVOID InformationBuffer, IN ULONG InformationBufferLength, OUT PULONG pBytesProcessed) {
NDISPROT_REQUEST ReqContext;
PNDIS_REQUEST pNdisRequest = &ReqContext.Request;
NDIS_STATUS Status;
// 初始化一个事件。这个事件会在请求完成函数中被设置,以便通知请求完成了。
NPROT_INIT_EVENT(&ReqContext.ReqEvent);
// 请求的类型。如果只是查询信息,只要用NdisRequestQueryInformation就可以了。
pNdisRequest->RequestType = RequestType;
// 根据不同的请求类型,填写OID和输入输出缓冲区。
switch (RequestType) {
case NdisRequestQueryInformation:
{
pNdisRequest->DATA.QUERY_INFORMATION.Oid = Oid;
pNdisRequest->DATA.QUERY_INFORMATION.InformationBuffer = InformationBuffer;
pNdisRequest->DATA.QUERY_INFORMATION.InformationBufferLength = InformationBufferLength;
break;
};
case NdisRequestSetInformation:
{
pNdisRequest->DATA.SET_INFORMATION.Oid = Oid;
pNdisRequest->DATA.SET_INFORMATION.InformationBuffer = InformationBuffer;
pNdisRequest->DATA.SET_INFORMATION.InformationBufferLength = InformationBufferLength;
break;
};
default:
{
NPROT_ASSERT(FALSE);
break;
};
};
// 发送请求
NdisRequest(&Status, pOpenContext->BindingHandle, pNdisRequest);
// 如果是未决,则等待事件。这个事件会在完成函数中设置。
if (Status == NDIS_STATUS_PENDING) {
NPROT_WAIT_EVENT(&ReqContext.ReqEvent, 0);
Status = ReqContext.Status;
};
// 如果成功了的话...
if (Status == NDIS_STATUS_SUCCESS) {
// 获得结果的长度。这个结果的长度是实际需要的长度。可能比我们实际提供的长度要长。
*pBytesProcessed = (RequestType == NdisRequestQueryInformation) ? pNdisRequest->DATA.QUERY_INFORMATION.BytesWritten : pNdisRequest->DATA.SET_INFORMATION.BytesRead;
// 如果结果长度比实际上我们提供的缓冲区要长,那么就简单的设置为输入参数中缓冲区的最大长度。
if (*pBytesProcessed > InformationBufferLength)
*pBytesProcessed = InformationBufferLength;
};
return Status;
};

完成函数的编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID NdisProtRequestComplete(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_REQUEST pNdisRequest, IN NDIS_STATUS Status) {
PNDISPROT_OPEN_CONTEXT pOpenContext;
PNDISPROT_REQUEST pReqContext;
// 这两句话起验证的作用,确保输入参数ProtocolBindingContext是合法的。但是对后面的处理没影响。
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// 从pNdisRequest中得到请求上下文
pReqContext = CONTAINING_RECORD(pNdisRequest, NDISPROT_REQUEST, Request);
// 保存结果状态
pReqContext->Status = Status;
// 设置事件
NPROT_SIGNAL_EVENT(&pReqContext->ReqEvent);
return;
};

解除绑定

当一个网卡被拔出时,内核调用协议特征集中的解除绑定回调函数来解除一个协议驱动和一个网卡的绑定。解除绑定用NdisCloseAdapter,原型如下。

1
2
3
4
VOID NdisCloseAdapter(
_Out_ PNDIS_STATUS Status,
_In_ NDIS_HANDLE NdisBindingHandle
);

NdisProtUnbindAdapter实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
VOID NdisProtUnbindAdapter(OUT PNDIS_STATUS pStatus, IN NDIS_HANDLE ProtocolBindingContext, IN NDIS_HANDLE UnbindContext)
/*++
Routine Description:
NDIS calls this when it wants us to close the binding to an adapter.
Arguments:
pStatus - place to return status of Unbind
ProtocolBindingContext - pointer to open context structure
UnbindContext - to use in NdisCompleteUnbindAdapter if we return pending
Return Value:
None
--*/
{
PNDISPROT_OPEN_CONTEXT pOpenContext;
UNREFERENCED_PARAMETER(UnbindContext);
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// Mark this open as having seen an Unbind.
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_UNBIND_FLAGS, NUIOO_UNBIND_RECEIVED); //无条件设置接收到解绑的标记
// In case we had threads blocked for the device below to be powered up, wake them up.
NPROT_SIGNAL_EVENT(&pOpenContext->PoweredUpEvent); //设置事件 通知所有在等待电源启动的线程 避免一些请求无法完成
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
ndisprotShutdownBinding(pOpenContext); //解绑
*pStatus = NDIS_STATUS_SUCCESS;
return;
};

其中ndisprotShutdownBinding的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
VOID ndisprotShutdownBinding(IN PNDISPROT_OPEN_CONTEXT pOpenContext) {
NDIS_STATUS Status;
BOOLEAN DoCloseBinding = FALSE;
do {
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 检查标记是否有NUIOO_BIND_OPENING,如果是正在打开的话,立刻退出。放弃解除绑定操作。
if (NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_OPENING)) {
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
break;
};
// 如果是绑定已经完成的情况,则设置为开始解除绑定,标记NUI_BIND_ACTIVE。其他的情况则根本不用做下面的操作。因为绑定既然没有完成,也当然不用解除绑定。
if (NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_ACTIVE)) {
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_CLOSING); //NUIOO_BIND_CLOSING已进入解绑过程
DoCloseBinding = TRUE;
};
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
if (DoCloseBinding) {
// 从这里开始解除绑定。
ULONG PacketFilter = 0;
ULONG BytesRead = 0;
// 把绑定了的网卡的包过滤器设置为0,换句话说,就是从现在开始停止收包。这是为了便于清理资源。
Status = ndisprotDoRequest(pOpenContext, NdisRequestSetInformation, OID_GEN_CURRENT_PACKET_FILTER, &PacketFilter, sizeof(PacketFilter), &BytesRead);
if (Status != NDIS_STATUS_SUCCESS)
DEBUGP(DL_WARN, ("ShutDownBinding: set packet filter failed: %x\n", Status));
// 把这个网卡的广播列表设置为NULL.
Status = ndisprotDoRequest(pOpenContext, NdisRequestSetInformation, OID_802_3_MULTICAST_LIST, NULL, 0, &BytesRead);
if (Status != NDIS_STATUS_SUCCESS)
DEBUGP(DL_WARN, ("ShutDownBinding: set multicast list failed: %x\n", Status));
// 取消所有的提交状态IRP。
ndisServiceIndicateStatusIrp(pOpenContext, 0, NULL, 0, TRUE);
// 等待所有未决IRP完成。
ndisprotWaitForPendingIO(pOpenContext, TRUE);
// 清理掉接收队列中所有的包。
ndisprotFlushReceiveQueue(pOpenContext);
// 初始化绑定事件,这个时间将用来等待解除绑定完成。
NPROT_INIT_EVENT(&pOpenContext->BindEvent);
DEBUGP(DL_INFO, ("ShutdownBinding: Closing OpenContext %p, BindingHandle %p\n", pOpenContext, pOpenContext->BindingHandle));
// 正式调用解除绑定。
NdisCloseAdapter(&Status, pOpenContext->BindingHandle);
if (Status == NDIS_STATUS_PENDING) {
NPROT_WAIT_EVENT(&pOpenContext->BindEvent, 0);
Status = pOpenContext->BindStatus;
};
NPROT_ASSERT(Status == NDIS_STATUS_SUCCESS);
pOpenContext->BindingHandle = NULL;
};
if (DoCloseBinding) {
// 设置上已经解除绑定的标记。
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_IDLE);
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_UNBIND_FLAGS, 0);
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
};
// 下面的操作主要是释放资源。
NPROT_ACQUIRE_LOCK(&Globals.GlobalLock);
NPROT_REMOVE_ENTRY_LIST(&pOpenContext->Link);
NPROT_RELEASE_LOCK(&Globals.GlobalLock);
ndisprotFreeBindResources(pOpenContext);
NPROT_DEREF_OPEN(pOpenContext); // Shutdown binding
} while (FALSE);
return;
};

用户态操作协议驱动

本小节为应用层程序代码。

标准的协议驱动可在用户层用Socket打开进行收发包的,但本例中抛弃了上层接口,简单地用ReadFileWriteFile进行收发包。ReadFile从应用层发出一个控制请求到某个设备,主功能号IRP_MJ_READ,WriteFile同理。DeviceIoControl调用时,设备收到一个控制请求,IRP主功能号IRP_MJ_DEVICE_CONTROL。

应用层基本步骤为:用CreateFile打开协议控制设备对象CDO,得到其句柄;用DeviceIoControl来等待绑定结束;用WriteFile发送数据包,用ReadFile接收数据包;用CloseHandle关闭句柄。本例驱动中设置的符号链接为“\\.\NdisProt”。

打开设备并发送控制请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
HANDLE OpenHandle(
__in__nullterminated PCHAR pDeviceName //符号链接名 以NULL结尾
) {
DWORD DesiredAccess;
DWORD ShareMode;
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL;
DWORD CreationDistribution;
DWORD FlagsAndAttributes;
HANDLE TemplateFile;
HANDLE Handle;
DWORD BytesReturned;
DesiredAccess = GENERIC_READ | GENERIC_WRITE;
ShareMode = 0;
CreationDistribution = OPEN_EXISTING;
FlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
TemplateFile = (HANDLE)INAVLID_HANDLE_VALUE;
Handle = CreateFile(pDeviceName, DesiredAccess, ShareMode, lpSecurityAttributes, CreationDistribution, FlagsAndAttributes, TemplateFile);
if (Handle == INVALID_HANDLE_VALUE)
return Handle;
if (!DeviceIoControl(Handle, IOCTL_NDISPROT_BIND_WAIT, NULL, 0, NULL, 0, &BytesReturned, NULL)) {
CloseHandle(Handle);
Handle = INVALID_HANDLE_VALUE;
};
return Handle;
};

其中DeviceIoControl原型:

1
2
3
4
5
6
7
8
BOOL DeviceIoControl(
HANDLE handle, //打开的句柄
DWORD control_code, //控制码 驱动中定义
PVOID input_buf, //输入缓冲区 可NULL
DWORD input_length, //输入缓冲区长度 可0
PVOID output_buf, //输出缓冲区 可NULL
DWORD output_length //输出缓冲区长度 可0
); //成功TRUE 失败FALSE

但此时仅用CreateFile打开了一个句柄,并输入了协议驱动的符号链接,还需要用DeviceIoControl传入设备名,控制码IOCTL_NDISPROT_OPEN_DEVICE。

1
2
3
4
5
6
7
8
9
10
11
12
WCHAR wNdisDeviceName[MAX_NDIS_DEVICE_NAME_LEN];
INT wNameLength;
INT NameLength = strlen(pDeviceName);
DWORD BytesReturned;
INT i;
wNameLength = 0;
for (i = 0; i < NameLength && i < MAX_NDIS_DEVICE_NAME_LEN - 1; i++) {
wNdisDeviceName[i] = (WCHAR)pDeviceName[i];
wNameLength++;
};
wNdisDeviceName[i] = L'\0';
DeviceIoControl(Handle, IOCTL_NDISPROT_OPEN_DEVICE, (LPVOID)&wndisDevicename[0], wnameLength * sizeof(WCHAR), NULL, 0, &BytesReturned, NULL);

接下来用WriteFile发送数据包,需要填写数据区和以太网包头。以太网包头有一个来源地址、一个目的地址、一个协议号。来源地址可以冒名顶替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pWriteBuf = malloc(PacketLength);
pEthHeader->EthType = EthType;
if (bUseFakeAddress)
memcpy(pEthHeader->SrcAddr, FakeSrcMacAddr, MAC_ADDR_LEN);
else
memcpy(pEthHeader->SrcAddr, SrcMacAddr, MAC_ADDR_LEN);
memcpy(pEthHeader->DstAddr, DstMacAddr, MAC_ADDR_LEN);
pData = (PUCHAR)(pEthHeader + 1);
for (i = 0; i < PacketLength - sizeof(ETH_HEADER); i++)
*pData++ = (UCHAR)i;
bSuccess = (BOOLEAN)WriteFile(Handle, pWriteBuf, PacketLength, &BytesWritten, NULL);
if (!bSuccess)
return;
if (pWriteBuf)
free(pWriteBuf);

WriteFileReadFile原型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL WriteFile(
HANDLE file_handle, //打开的设备句柄
PVOID packet_buf, //要写入内容缓冲区
DWORD packet_length, //缓冲区长度
PDWORD written_length, //返回实际成功发出的长度
PLARGE_INTEGER offset //写文件的偏移 这里发送包用不上 填NULL
);
BOOL ReadFile(
HANDLE file_handle,
PVOID packet_buf,
DWORD packet_length,
PDWORD written_length,
PLARGE_INTEGER offset
);

读取数据包方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUCHAR pReadBuf = NULL;
INT ReadCount = 0;
BOOLEAN bSuccess;
ULONG BytesRead;
do {
pReadBuf = malloc(PacketLength);
if (pReadBuf == NULL)
break;
ReadCount = 0; //读出包的个数
while (TRUE) {
bSuccess = (BOOLEAN)ReadFile(Handle, (LPVOID)pReadBuf, PacketLength, &BytesRead, NULL);
if (!bSuccess)
break;
ReadCount++;
if ((NumberOfPackets != -1) && (ReadCount == NumberOfPackets))
break;
};
} while (FALSE);
if (pReadBuf)
free(pReadBuf);

内核态功能实现

在驱动入口函数有设置各类分发函数。分发函数NdisProtIoControl处理所有主功能号为IRP_MJ_DEVICE_CONTROL的IRP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
NTSTATUS NdisProtIoControl(IN PDEVICE_OBJECT pDeviceObject,IN PIRP pIrp)
/*++
Routine Description:
This is the dispatch routine for handling device ioctl requests.
Arguments:
pDeviceObject - Pointer to the device object.
pIrp - Pointer to the request packet.
Return Value:
Status is returned.
--*/
{
PIO_STACK_LOCATION pIrpSp;
ULONG FunctionCode;
NTSTATUS NtStatus;
NDIS_STATUS Status;
PNDISPROT_OPEN_CONTEXT pOpenContext;
ULONG BytesReturned;
USHORT EthType;
#if !DBG
UNREFERENCED_PARAMETER(pDeviceObject);
#endif
DEBUGP(DL_LOUD, ("IoControl: DevObj %p, Irp %p\n", pDeviceObject, pIrp));
pIrpSp = IoGetCurrentIrpStackLocation(pIrp);
FunctionCode = pIrpSp->Parameters.DeviceIoControl.IoControlCode;
pOpenContext = (PNDISPROT_OPEN_CONTEXT)pIrpSp->FileObject->FsContext;
BytesReturned = 0;
switch (FunctionCode) {
case IOCTL_NDISPROT_BIND_WAIT:
{
// 所有的DeviceIoControl请求的都应该是用的缓冲方式。这里只是确认一下。
NPROT_ASSERT((FunctionCode & 0x3) == METHOD_BUFFERED);
// 非常简单。等待一个全局事件。这个全局变量会在绑定完成的时候被设置。如果等待到了或者超时了(5秒)则返回。
if (NPROT_WAIT_EVENT(&Globals.BindsComplete, 5000))
NtStatus = STATUS_SUCCESS;
else
NtStatus = STATUS_TIMEOUT;
DEBUGP(DL_INFO, ("IoControl: BindWait returning %x\n", NtStatus));
break;
};
case IOCTL_NDISPROT_QUERY_BINDING:
{
NPROT_ASSERT((FunctionCode & 0x3) == METHOD_BUFFERED);
Status = ndisprotQueryBinding(pIrp->AssociatedIrp.SystemBuffer, pIrpSp->Parameters.DeviceIoControl.InputBufferLength, pIrpSp->Parameters.DeviceIoControl.OutputBufferLength, &BytesReturned);
NDIS_STATUS_TO_NT_STATUS(Status, &NtStatus);
DEBUGP(DL_LOUD, ("IoControl: QueryBinding returning %x\n", NtStatus));
break;
};
case IOCTL_NDISPROT_OPEN_DEVICE:
{
NPROT_ASSERT((FunctionCode & 0x3) == METHOD_BUFFERED);
if (pOpenContext != NULL) {
NPROT_STRUCT_ASSERT(pOpenContext, oc);
DEBUGP(DL_WARN, ("IoControl: OPEN_DEVICE: FileObj %p alreadyassociated with open %p\n", pIrpSp->FileObject, pOpenContext));
NtStatus = STATUS_DEVICE_BUSY;
break;
};
NtStatus = ndisprotOpenDevice(pIrp->AssociatedIrp.SystemBuffer, pIrpSp->Parameters.DeviceIoControl.InputBufferLength, pIrpSp->FileObject, &pOpenContext);
if (NT_SUCCESS(NtStatus))
DEBUGP(DL_VERY_LOUD, ("IoControl OPEN_DEVICE: Open %p <-> FileObject %p\n", pOpenContext, pIrpSp->FileObject));
break;
};
case IOCTL_NDISPROT_QUERY_OID_VALUE:
{
NPROT_ASSERT((FunctionCode & 0x3) == METHOD_BUFFERED);
if (pOpenContext != NULL) {
Status = ndisprotQueryOidValue(pOpenContext, pIrp->AssociatedIrp.SystemBuffer, pIrpSp->Parameters.DeviceIoControl.OutputBufferLength, &BytesReturned);
NDIS_STATUS_TO_NT_STATUS(Status, &NtStatus);
}
else
NtStatus = STATUS_DEVICE_NOT_CONNECTED;
break;
};
case IOCTL_NDISPROT_SET_OID_VALUE:
{
NPROT_ASSERT((FunctionCode & 0x3) == METHOD_BUFFERED);
if (pOpenContext != NULL) {
Status = ndisprotSetOidValue(pOpenContext, pIrp->AssociatedIrp.SystemBuffer, pIrpSp->Parameters.DeviceIoControl.InputBufferLength);
BytesReturned = 0;
NDIS_STATUS_TO_NT_STATUS(Status, &NtStatus);
}
else
NtStatus = STATUS_DEVICE_NOT_CONNECTED;
break;
};
case IOCTL_NDISPROT_INDICATE_STATUS:
{
NPROT_ASSERT((FunctionCode & 0x3) == METHOD_BUFFERED);
if (pOpenContext != NULL)
NtStatus = ndisprotQueueStatusIndicationIrp(pOpenContext, pIrp, &BytesReturned);
else
NtStatus = STATUS_DEVICE_NOT_CONNECTED;
break;
};
default:
{
NtStatus = STATUS_NOT_SUPPORTED;
break;
};
};
if (NtStatus != STATUS_PENDING) {
pIrp->IoStatus.Information = BytesReturned;
pIrp->IoStatus.Status = NtStatus;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
};
return NtStatus;
};

ndisprotOpenDevice实现步骤有:从输入缓冲区拿到设备名、通过设备名去找对应的打开上下文、找到后保存在pIrpSp->FileObject->FsContext。在文件系统中FsContext保存FCB,但其他驱动中可以提供“文件”对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
NTSTATUS ndisprotOpenDevice(
__in_bcount(DeviceNameLength) IN PUCHAR pDeviceName,// pDeviceName 设备对象名
IN ULONG DeviceNameLength,// DeviceNameLength 设备对象的长度
IN PFILE_OBJECT pFileObject,// pFileObject 文件对象指针
OUT PNDISPROT_OPEN_CONTEXT* ppOpenContext
) {
PNDISPROT_OPEN_CONTEXT pOpenContext;
NTSTATUS NtStatus;
ULONG PacketFilter;
NDIS_STATUS NdisStatus;
ULONG BytesProcessed;
PNDISPROT_OPEN_CONTEXT pCurrentOpenContext = NULL;
pOpenContext = NULL;
do {
// 根据设备名找到打开上下文。请注意这个中间会调用增加打开上下文引用计数,所以后面要解引用。
pOpenContext = ndisprotLookupDevice(pDeviceName, DeviceNameLength);
// 如果找不到打开上下文,说明没绑定过...
if (pOpenContext == NULL) {
DEBUGP(DL_WARN, ("ndisprotOpenDevice: couldn't find device\n"));
NtStatus = STATUS_OBJECT_NAME_NOT_FOUND;
break;
};
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 如果找到了,但是不是打开空闲状态,则返回设备忙。
if (!NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_OPEN_FLAGS, NUIOO_OPEN_IDLE)) {
NPROT_ASSERT(pOpenContext->pFileObject != NULL);
DEBUGP(DL_WARN, ("ndisprotOpenDevice: Open %p/%x already associated with another FileObject %p\n", pOpenContext, pOpenContext->Flags, pOpenContext->pFileObject));
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 注意解引用。
NPROT_DEREF_OPEN(pOpenContext); // ndisprotOpenDevice failure
NtStatus = STATUS_DEVICE_BUSY;
break;
};
// 比较交换。首先比较pFileObject->FsContext和NULL.如果是NULL,则用pFileObject->FsContext设置为pOpenContext,然后返回NULL。如果不是NULL,则不交换,并返回pFileObject->FsContext
if ((pCurrentOpenContext = InterlockedCompareExchangePointer(&(pFileObject->FsContext), pOpenContext, NULL)) != NULL) {
// 到了这里,说明另一个打开已经使用了这个文件对象。这个设备不支持两次打开。到这里直接返回失败即可。
DEBUGP(DL_WARN, ("ndisprotOpenDevice: FileObject %p already associated with another Open %p/%x\n", pFileObject, pCurrentOpenContext, pCurrentOpenContext->Flags)); //BUG
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NPROT_DEREF_OPEN(pOpenContext); // ndisprotOpenDevice failure
NtStatus = STATUS_INVALID_DEVICE_REQUEST;
break;
};
// 这个打开上下文被打开了,保存在这个文件对象的FsContext中。这里也保存
pOpenContext->pFileObject = pFileObject;
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_OPEN_FLAGS, NUIOO_OPEN_ACTIVE);
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 设置PacketFilter,使之能收到包。
PacketFilter = NUIOO_PACKET_FILTER;
NdisStatus = ndisprotValidateOpenAndDoRequest(pOpenContext, NdisRequestSetInformation, OID_GEN_CURRENT_PACKET_FILTER, &PacketFilter, sizeof(PacketFilter), &BytesProcessed, TRUE); // TRUE:Do wait for power on
// 不成功的话
if (NdisStatus != NDIS_STATUS_SUCCESS) {
DEBUGP(DL_WARN, ("openDevice: Open %p: set packet filter (%x) failed: %x\n", pOpenContext, PacketFilter, NdisStatus));
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 不成功的话,去掉FileObject->FsContext设置,如果pFileObject->FsContext是pOpenContext,则去掉。
pCurrentOpenContext = InterlockedCompareExchangePointer(&(pFileObject->FsContext), NULL, pOpenContext);
NPROT_ASSERT(pCurrentOpenContext == pOpenContext);
NPROT_SET_FLAGS(pOpenContext->Flags, NUIOO_OPEN_FLAGS, NUIOO_OPEN_IDLE);
pOpenContext->pFileObject = NULL;
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NPROT_DEREF_OPEN(pOpenContext); // ndisprotOpenDevice failure
NDIS_STATUS_TO_NT_STATUS(NdisStatus, &NtStatus);
break;
};
// 返回打开上下文。
*ppOpenContext = pOpenContext;
NtStatus = STATUS_SUCCESS;
} while (FALSE);
return (NtStatus);
};

读请求就是从应用层获取网卡收到的包,这些包被本协议驱动放入缓冲队列中。处理读请求就是检测队列中有无数据包,有则把包内容拷贝到读请求输出缓冲区中。若存在多个网卡,则把打开上下文取出,就能知道调用者需要收包的是哪个网卡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 分发函数之一,处理读请求。
NTSTATUS NdisProtRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp) {
PIO_STACK_LOCATION pIrpSp;
NTSTATUS NtStatus;
PNDISPROT_OPEN_CONTEXT pOpenContext;
UNREFERENCED_PARAMETER(pDeviceObject);
pIrpSp = IoGetCurrentIrpStackLocation(pIrp);
pOpenContext = pIrpSp->FileObject->FsContext;
do {
// 检测打开上下文的可靠性
if (pOpenContext == NULL) {
DEBUGP(DL_FATAL, ("Read: NULL FsContext on FileObject %p\n", pIrpSp->FileObject));
NtStatus = STATUS_INVALID_HANDLE;
break;
};
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// Read和Write都是使用的直接IO操作,所以应该使用MdlAddress来传递缓冲。如果不是,返回非法参数错误。
if (pIrp->MdlAddress == NULL) {
DEBUGP(DL_FATAL, ("Read: NULL MDL address on IRP %p\n", pIrp));
NtStatus = STATUS_INVALID_PARAMETER;
break;
};
// 得到缓冲的虚拟地址。
if (MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority) == NULL) {
DEBUGP(DL_FATAL, ("Read: MmGetSystemAddr failed for IRP %p, MDL %p\n", pIrp, pIrp->MdlAddress));
NtStatus = STATUS_INSUFFICIENT_RESOURCES;
break;
};
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
if (!NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_ACTIVE)) {
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NtStatus = STATUS_INVALID_HANDLE;
break;
};
// 将这个请求插入处理队列里。并把打开上下文引用计数增加就1.把未处理读请求数目增加1.
NPROT_INSERT_TAIL_LIST(&pOpenContext->PendedReads, &pIrp->Tail.Overlay.ListEntry);
NPROT_REF_OPEN(pOpenContext); // pended read IRP
pOpenContext->PendedReadCount++;
// 标记IRP未决。给IRP设置一个取消函数,使之变得可取消。
pIrp->Tail.Overlay.DriverContext[0] = (PVOID)pOpenContext;
IoMarkIrpPending(pIrp);
IoSetCancelRoutine(pIrp, NdisProtCancelRead);
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NtStatus = STATUS_PENDING;
// 调用一个处理例程来处理所有未决的读请求。
ndisprotServiceReads(pOpenContext);
} while (FALSE);
// 读请求只返回STATUS_PENDING.如果不是,则说明出错,按错误返回。
if (NtStatus != STATUS_PENDING) {
NPROT_ASSERT(NtStatus != STATUS_SUCCESS);
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = NtStatus;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
};
return (NtStatus);
};

写请求就是发包请求,处理代码在NdisProtWrite中,主请求号IRP_MJ_WRITE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 分发函数,处理写请求(也就是发包请求)。
NTSTATUS NdisProtWrite(IN PDEVICE_OBJECT pDeviceObject,IN PIRP pIrp) {
PIO_STACK_LOCATION pIrpSp;
ULONG DataLength;
NTSTATUS NtStatus;
NDIS_STATUS Status;
PNDISPROT_OPEN_CONTEXT pOpenContext;
PNDIS_PACKET pNdisPacket;
PNDIS_BUFFER pNdisBuffer;
NDISPROT_ETH_HEADER UNALIGNED* pEthHeader;
// NDIS51支持写请求取消。但是本书不讨论请求取消的话题。
#ifdef NDIS51
PVOID CancelId;
#endif
UNREFERENCED_PARAMETER(pDeviceObject);
// 首先得到打开上下文。以确认是用哪个网卡发包。
pIrpSp = IoGetCurrentIrpStackLocation(pIrp);
pOpenContext = pIrpSp->FileObject->FsContext;
pNdisPacket = NULL;
do {
// 检查打开上下文的可靠性。
if (pOpenContext == NULL) {
DEBUGP(DL_WARN, ("Write: FileObject %p not yet associated with a device\n", pIrpSp->FileObject));
NtStatus = STATUS_INVALID_HANDLE;
break;
};
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// 确认输入缓冲的可靠性。
if (pIrp->MdlAddress == NULL) {
DEBUGP(DL_FATAL, ("Write: NULL MDL address on IRP %p\n", pIrp));
NtStatus = STATUS_INVALID_PARAMETER;
break;
};
// 得到输入缓冲的虚拟地址。之后进行一系列的检查。第一,输入缓冲虚拟地址不能为NULL,第二,缓冲的长度,至少必须比一个以太网包头要长。否则无法填写以太网包头。第三,发包的长度不能超过这个网卡的最大帧长。第四,
pEthHeader = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
if (pEthHeader == NULL) {
DEBUGP(DL_FATAL, ("Write: MmGetSystemAddr failed for IRP %p, MDL %p\n", pIrp, pIrp->MdlAddress));
NtStatus = STATUS_INSUFFICIENT_RESOURCES;
break;
};
DataLength = MmGetMdlByteCount(pIrp->MdlAddress);
if (DataLength < sizeof(NDISPROT_ETH_HEADER)) {
DEBUGP(DL_WARN, ("Write: too small to be a valid packet (%d bytes)\n", DataLength));
NtStatus = STATUS_BUFFER_TOO_SMALL;
break;
};
if (DataLength > (pOpenContext->MaxFrameSize + sizeof(NDISPROT_ETH_HEADER))) {
DEBUGP(DL_WARN, ("Write: Open %p: data length (%d) larger than max frame size (%d)\n", pOpenContext, DataLength, pOpenContext->MaxFrameSize));
NtStatus = STATUS_INVALID_BUFFER_SIZE;
break;
};
// 下面开始检查,缓冲中是否已经填写了伪造的MAC地址。方法很简单,取得已填写的地址和网卡的MAC地址比较。如果不符合则返回失败。很多情况,网络攻击工具是不会拷贝这段代码的。
if ((pIrp->RequestorMode == UserMode) && !NPROT_MEM_CMP(pEthHeader->SrcAddr, pOpenContext->CurrentAddress, NPROT_MAC_ADDR_LEN)) {
DEBUGP(DL_WARN, ("Write: Failing with invalid Source address"));
NtStatus = STATUS_INVALID_PARAMETER;
break;
};
// 确认包可以发送了。下面开始真实的准备发送一个包,首先获得锁,并判断当前网卡是否处于可以发包的状态。
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
if (!NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_ACTIVE)) {
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
DEBUGP(DL_FATAL, ("Write: Open %p is not bound or in low power state\n", pOpenContext));
NtStatus = STATUS_INVALID_HANDLE;
break;
};
// 从前面绑定时分配的发送包池中分配一个包描述符。
NPROT_ASSERT(pOpenContext->SendPacketPool != NULL);
NdisAllocatePacket(&Status,&pNdisPacket,pOpenContext->SendPacketPool);
if (Status != NDIS_STATUS_SUCCESS) {
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
DEBUGP(DL_FATAL, ("Write: open %p, failed to alloc send pkt\n", pOpenContext));
NtStatus = STATUS_INSUFFICIENT_RESOURCES;
break;
};
// 下面的代码为Win9x编写,本书不讨论。
if (pOpenContext->bRunningOnWin9x) {
NdisAllocateBuffer(&Status,&pNdisBuffer,pOpenContext->SendBufferPool,pEthHeader,DataLength);
if (Status != NDIS_STATUS_SUCCESS) {
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NdisFreePacket(pNdisPacket);
DEBUGP(DL_FATAL, ("Write: open %p, failed to alloc send buf\n", pOpenContext));
NtStatus = STATUS_INSUFFICIENT_RESOURCES;
break;
};
}
else
pNdisBuffer = pIrp->MdlAddress;
// 记录发送包又增加了一个。
NdisInterlockedIncrement((PLONG)&pOpenContext->PendedSendCount);
// 打开上下文引用计数加1,这是为了防止发包过程中这个绑定被解除。
NPROT_REF_OPEN(pOpenContext); // pended send
IoMarkIrpPending(pIrp);
// 初始化包引用计数。这个包会在计数为0的时候释放掉。
NPROT_SEND_PKT_RSVD(pNdisPacket)->RefCount = 1;
#ifdef NDIS51
// NDIS5.1支持取消发送。我们给每个包设置一个取消ID。每个包和一个写IRP关联,把包的指针保存在IRP中。如果IRP获得取消通知,则使用NdisCancelSendPackets去取消包。
CancelId = NPROT_GET_NEXT_CANCEL_ID();
NDIS_SET_PACKET_CANCEL_ID(pNdisPacket, CancelId);
pIrp->Tail.Overlay.DriverContext[0] = (PVOID)pOpenContext;
pIrp->Tail.Overlay.DriverContext[1] = (PVOID)pNdisPacket;
NPROT_INSERT_TAIL_LIST(&pOpenContext->PendedWrites, &pIrp->Tail.Overlay.ListEntry);
IoSetCancelRoutine(pIrp, NdisProtCancelWrite);
#endif // NDIS51
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 记下irp的指针放在包描述符里,以备后用。
NPROT_IRP_FROM_SEND_PKT(pNdisPacket) = pIrp;
// 把缓冲和包联系起来。状态设置为pending。
NtStatus = STATUS_PENDING;
pNdisBuffer->Next = NULL;
NdisChainBufferAtFront(pNdisPacket, pNdisBuffer);
// 下面的代码仅供调试使用。
#if SEND_DBG{
PUCHAR pData;
pData = MmGetSystemAddressForMdlSafe(pNdisBuffer, NormalPagePriority);
NPROT_ASSERT(pEthHeader == pData);
DEBUGP(DL_VERY_LOUD,("Write: MDL %p, MdlFlags %x, SystemAddr %p, %d bytes\n",pIrp->MdlAddress, pIrp->MdlAddress->MdlFlags, pData, DataLength));
DEBUGPDUMP(DL_VERY_LOUD, pData, MIN(DataLength, 48));
}
#endif // SEND_DBG
// 发送包。非常简单。包发送完之后会自动调用协议特征中的一个回调函数NdisProtSendComplete。在其中再完成IRP即可。
NdisSendPackets(pOpenContext->BindingHandle, &pNdisPacket, 1);
} while (FALSE);
// 如果正常发送包是STATUS_PENDING。否则是有错的,可以在这里直接完成。
if (NtStatus != STATUS_PENDING) {
pIrp->IoStatus.Status = NtStatus;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
};
return NtStatus;
};

包发送NdisProtSendComplete的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 这是协议特征集中的一个回调函数。如果调用了NdisSendPacket,那么在发送结束之后,这个函数会被调用。
VOID NdisProtSendComplete(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_PACKET pNdisPacket, IN NDIS_STATUS Status) {
PIRP pIrp;
PIO_STACK_LOCATION pIrpSp;
PNDISPROT_OPEN_CONTEXT pOpenContext;
// 取得打开上下文。
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// 从包描述符中取得IRP的指针。
pIrp = NPROT_IRP_FROM_SEND_PKT(pNdisPacket);
// 下面的代码只和Win9x有关。本书不讨论。
if (pOpenContext->bRunningOnWin9x) {
// We would have attached our own NDIS_BUFFER. Take it out and free it.
#ifndef NDIS51
PNDIS_BUFFER pNdisBuffer;
PVOID VirtualAddr;
UINT BufferLength;
UINT TotalLength;
#endif
#ifdef NDIS51
NPROT_ASSERT(FALSE); // NDIS 5.1 not on Win9X!
#else
NdisGetFirstBufferFromPacket(pNdisPacket, &pNdisBuffer, &VirtualAddr, &BufferLength, &TotalLength);
NPROT_ASSERT(pNdisBuffer != NULL);
NdisFreeBuffer(pNdisBuffer);
#endif
};
// 去掉未决取消函数。同时从未决链中删除。
#ifdef NDIS51
IoSetCancelRoutine(pIrp, NULL);
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
NPROT_REMOVE_ENTRY_LIST(&pIrp->Tail.Overlay.ListEntry);
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
#endif
// 数据包解引用。
NPROT_DEREF_SEND_PKT(pNdisPacket);
// 把请求完成掉。
pIrpSp = IoGetCurrentIrpStackLocation(pIrp);
if (Status == NDIS_STATUS_SUCCESS) {
pIrp->IoStatus.Information = pIrpSp->Parameters.Write.Length;
pIrp->IoStatus.Status = STATUS_SUCCESS;
}
else {
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
};
DEBUGP(DL_INFO, ("SendComplete: packet %p/IRP %p/Length %d completed with status %x\n", pNdisPacket, pIrp, pIrp->IoStatus.Information, pIrp->IoStatus.Status));
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
// 未决发送包减少一个。
NdisInterlockedDecrement((PLONG)&pOpenContext->PendedSendCount);
NPROT_DEREF_OPEN(pOpenContext);
return;
};

协议驱动接收回调

当被绑定的网卡收到数据包时,内核调用ReceiveHandlerReceivePacketHandler,至于啥时候调用哪个尚不明朗。ReceiveCompleteHandler回调函数在ReceiveHandler调用完后。若只需要包的数据内容的前几字节就可决定该包是否是本协议需要处理的,那么下层驱动就没必要提交整个包,只提供包开始的几个字节给协议驱动,这称为前视区。若决定获取完整数据,应用NdisTransferData,传输完成后回调函数TransferDataCompleteHandler将被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
NDIS_STATUS NdisProtReceive(
IN NDIS_HANDLE ProtocolBindingContext, //绑定句柄
IN NDIS_HANDLE MacReceiveContext,
__in_bcount(HeaderBufferSize) IN PVOID pHeaderBuffer, //以太网包头
IN UINT HeaderBufferSize, //包头长度 一般14
__in_bcount(LookaheadBufferSize) IN PVOID pLookaheadBuffer, //前视区
IN UINT LookaheadBufferSize, //前视区长度
IN UINT PacketSize //完整包长度
)
/*++
Routine Description:
Our protocol receive handler called by NDIS, typically if we have a miniport below that doesn't indicate packets.
We make a local packet/buffer copy of this data, queue it up, and kick off the read service routine.
Arguments:
ProtocolBindingContext - pointer to open context
MacReceiveContext - for use in NdisTransferData
pHeaderBuffer - pointer to data header
HeaderBufferSize - size of the above
pLookaheadBuffer - pointer to buffer containing lookahead data
LookaheadBufferSize - size of the above
PacketSize - size of the entire packet, minus header size.
Return Value:
NDIS_STATUS_NOT_ACCEPTED - if this packet is uninteresting
NDIS_STATUS_SUCCESS - if we processed this successfully
--*/
{
PNDISPROT_OPEN_CONTEXT pOpenContext;
NDIS_STATUS Status;
PNDIS_PACKET pRcvPacket;
PUCHAR pRcvData;
UINT BytesTransferred;
PNDIS_BUFFER pOriginalNdisBuffer, pPartialNdisBuffer;
// 获得绑定句柄。
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
pRcvPacket = NULL;
pRcvData = NULL;
Status = NDIS_STATUS_SUCCESS;
do {
// 如果头长度不是以太网包头的长度,则不接收这个包。本协议只接收以太网包。
if (HeaderBufferSize != sizeof(NDISPROT_ETH_HEADER)) {
Status = NDIS_STATUS_NOT_ACCEPTED;
break;
};
// 这个比较比较奇怪。难道头长度是负数?
if ((PacketSize + HeaderBufferSize) < PacketSize) {
Status = NDIS_STATUS_NOT_ACCEPTED;
break;
};
// 分配一个包。包括包描述符和缓冲描述符,以及内存空间,一次性分配好。
pRcvPacket = ndisprotAllocateReceivePacket(pOpenContext, PacketSize + HeaderBufferSize, &pRcvData);
// 如果分配失败了,就不再接收这个包了。
if ((pRcvPacket == NULL) || (pRcvData == NULL)) {
Status = NDIS_STATUS_NOT_ACCEPTED;
break;
};
// 内存拷贝。先拷贝以太网包头。
NdisMoveMappedMemory(pRcvData, pHeaderBuffer, HeaderBufferSize);
// 检查前视区里是否包含了完整包的数据。
if (PacketSize == LookaheadBufferSize) {
// 如果前视区已经包括了整个数据包,那么调用NdisCopyLookaheadData就得到了完整的包,然后调用ndisprotQueueReceivePacket将这个包插入队列即可。
NdisCopyLookaheadData(pRcvData + HeaderBufferSize, pLookaheadBuffer, LookaheadBufferSize, pOpenContext->MacOptions);
ndisprotQueueReceivePacket(pOpenContext, pRcvPacket);
}
else {
// 否则的话,需要分配一个新的缓冲描述符。请注意这个描述符号对应的是从包缓冲区开始之后HeaderBufferSize个字节之后处开始的空间(pRcvData + HeaderBufferSize)。
NdisAllocateBuffer(&Status, &pPartialNdisBuffer, pOpenContext->RecvBufferPool, pRcvData + HeaderBufferSize, PacketSize);
if (Status == NDIS_STATUS_SUCCESS) {
// 如果成功了,就把包上原有的缓冲解链。使原来的缓冲描述符脱离包描述符。
NdisUnchainBufferAtFront(pRcvPacket, &pOriginalNdisBuffer);
// 现在把原来的包描述符保存在包描述符中(保留以备后用)
NPROT_RCV_PKT_TO_ORIGINAL_BUFFER(pRcvPacket) = pOriginalNdisBuffer;
// 然后把新的缓冲描述符连接到包上。
NdisChainBufferAtBack(pRcvPacket, pPartialNdisBuffer);
DEBUGP(DL_LOUD, ("Receive: setting up for TransferData: Pkt %p, OriginalBuf %p, PartialBuf %p\n", pRcvPacket, pOriginalNdisBuffer, pPartialNdisBuffer));
// 然后调用NdisTransferData来传输数据包剩余的部分。这个调用完成之后,协议特征中的NdisProtTransferDataComplete会被调用。
NdisTransferData(&Status, pOpenContext->BindingHandle, MacReceiveContext, 0, PacketSize, pRcvPacket, &BytesTransferred);
}
else
// 如果失败了,就不会调用NdisTransferData。但是我们还是要在NdisProtTransferDataComplete中做最后的处理。所以自己填写BytesTransferred。
BytesTransferred = 0;
if (Status != NDIS_STATUS_PENDING)
// 如果前面就失败了,我们自己调用NdisProtTransferDataComplete。
NdisProtTransferDataComplete((NDIS_HANDLE)pOpenContext, pRcvPacket, Status, BytesTransferred);
};
} while (FALSE);
DEBUGP(DL_LOUD, ("Receive: Open %p, Pkt %p, Size %d\n", pOpenContext, pRcvPacket, PacketSize));
return Status;
};

另一种类型的接收回调函数指针ReceivePacketHandler的原型为:

1
2
3
4
INT NdisProtReceivePacket(
_In_ NDIS_HANDLE ProtocolBindingContext,
_In_ PNDIS_PACKET pNdisPacket
);

上述TransferDataCompleteHandler实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
VOID NdisProtTransferDataComplete(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_PACKET pNdisPacket, IN NDIS_STATUS TransferStatus, IN UINT BytesTransferred)
/*++
Routine Description:
NDIS entry point called to signal completion of a call to NdisTransferData that had pended.
Arguments:
ProtocolBindingContext - pointer to open context
pNdisPacket - our receive packet into which data is transferred
TransferStatus - status of the transfer
BytesTransferred - bytes copied into the packet.
Return Value:
None
--*/
{
PNDISPROT_OPEN_CONTEXT pOpenContext;
PNDIS_BUFFER pOriginalBuffer, pPartialBuffer;
UNREFERENCED_PARAMETER(BytesTransferred);
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// 得到保存过的旧的缓冲描述符。要记得在传输之前,为了让传输的内容正确的写到以太网包头后,我们分配了一个新的缓冲描述符替换了旧的缓冲描述符。现在要恢复它了。
pOriginalBuffer = NPROT_RCV_PKT_TO_ORIGINAL_BUFFER(pNdisPacket);
if (pOriginalBuffer != NULL) {
// 和前面的替换时的操作一样,先Unchain,然后再调用Chain。调用之后已经恢复了使用旧的包描述符。
NdisUnchainBufferAtFront(pNdisPacket, &pPartialBuffer);
NdisChainBufferAtBack(pNdisPacket, pOriginalBuffer);
DEBUGP(DL_LOUD, ("TransferComp: Pkt %p, OrigBuf %p, PartialBuf %p\n", pNdisPacket, pOriginalBuffer, pPartialBuffer));
ASSERT(pPartialBuffer != NULL);
// 那么那个新的包描述符已经没用了,调用NdisFreeBuffer释放它。
if (pPartialBuffer != NULL)
NdisFreeBuffer(pPartialBuffer);
};
if (TransferStatus == NDIS_STATUS_SUCCESS)
// 如果传输是成功的,将包保存到接收队列中。
ndisprotQueueReceivePacket(pOpenContext, pNdisPacket);
else
// 如果传输失败了,直接释放这个包。
ndisprotFreeReceivePacket(pOpenContext, pNdisPacket);
return;
};

这里ReveicePacketHandler返回值为引用计数,即接收到的包描述符被本协议驱动使用的次数。若包被本驱动使用,则下层网卡不能释放这个包。引用计数为0才能释放。若要重用该网络包,就要返回一个引用计数,一般1就行。下层驱动资源紧张时,必需释放该包并重新分配,拷贝到新的包描述符中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
INT NdisProtReceivePacket(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_PACKET pNdisPacket)
/*++
Routine Description:
Protocol entry point called by NDIS if the driver below uses NDIS 4 style receive packet indications.
If the miniport allows us to hold on to this packet, we use it as is, otherwise we make a copy.
Arguments:
ProtocolBindingContext - pointer to open context
pNdisPacket - the packet being indicated up.
Return Value:
None
--*/
{
PNDISPROT_OPEN_CONTEXT pOpenContext;
PNDIS_BUFFER pNdisBuffer;
UINT BufferLength;
PNDISPROT_ETH_HEADER pEthHeader;
PNDIS_PACKET pCopyPacket;
PUCHAR pCopyBuf;
UINT TotalPacketLength;
UINT BytesCopied;
INT RefCount = 0;
NDIS_STATUS Status;
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
#ifdef NDIS51
NdisGetFirstBufferFromPacketSafe(pNdisPacket, &pNdisBuffer, &pEthHeader, &BufferLength, &TotalPacketLength, NormalPagePriority);
if (pEthHeader == NULL)
// The system is low on resources. Set up to handle failure below.
BufferLength = 0;
#else
// 从包描述符中得到第一个缓冲描述符。
NdisGetFirstBufferFromPacket(pNdisPacket, &pNdisBuffer, &pEthHeader, &BufferLength, &TotalPacketLength);
#endif
do {
// 如果这个包的长度比以太网包头还要小,丢弃之。
if (BufferLength < sizeof(NDISPROT_ETH_HEADER)) {
DEBUGP(DL_WARN, ("ReceivePacket: Open %p, runt pkt %p, first buffer length %d\n", pOpenContext, pNdisPacket, BufferLength));
Status = NDIS_STATUS_NOT_ACCEPTED;
break;
};
DEBUGP(DL_LOUD, ("ReceivePacket: Open %p, interesting pkt %p\n", pOpenContext, pNdisPacket));
// 如果这个包有NDIS_STATUS_RESOURCES状态,则必须拷贝而不能重用该包。当然这样就比较消耗时间和资源了。
if ((NDIS_GET_PACKET_STATUS(pNdisPacket) == NDIS_STATUS_RESOURCES) || pOpenContext->bRunningOnWin9x) {
// 下面是分配一个包并拷贝其内容。读者可以参考前面讲过的内容来理解。
pCopyPacket = ndisprotAllocateReceivePacket(pOpenContext, TotalPacketLength, &pCopyBuf);
if (pCopyPacket == NULL) {
DEBUGP(DL_FATAL, ("ReceivePacket: Open %p, failed to alloc copy, %d bytes\n", pOpenContext, TotalPacketLength));
break;
};
// 调用NdisCopyFromPacketToPacket来拷贝包。当然在拷贝之前调用者必须确保目标包的缓冲区长度是足够的。
NdisCopyFromPacketToPacket(pCopyPacket, 0, TotalPacketLength, pNdisPacket, 0, &BytesCopied);
NPROT_ASSERT(BytesCopied == TotalPacketLength);
// 那么现在开始就用新的包了。
pNdisPacket = pCopyPacket;
}
else
// 返回值。返回值表示的是我们已经一次引用了这个包。当处理完结的时候,我们就可以调用NdisReturnPackets来要求下层驱动释放这个包了。本函数把RefCount当做返回值。如果返回了0,那么下层驱动会认为我们不再需要这个数据包。
RefCount = 1;
// 将数据包放入队列里。
ndisprotQueueReceivePacket(pOpenContext, pNdisPacket);
} while (FALSE);
return RefCount;
};

上面这俩接收包的回调函数最终都是收到一个数据包并把它放入队列中。该队列用LIST_ENTRY实现,还有一些常用宏:

1
2
3
4
#define NPROT_INSERT_TAIL_LIST(_pList, _pEnt)    InsertTailList(_pList, _pEnt) //插入链表
#define NPROT_REMOVE_ENTRY_LIST(_pEnt) RemoveEntryList(_pEnt)
#define NPROT_LIST_ENTRY_TO_RCV_PKT(_pEnt) CONTAINING_RECORD(CONTAINING_RECORD(_pEnt, NPROT_RECV_PACKET_RSVD, Link), NDIS_PACKET, ProtocolReserved) //从链表节点指针得到包指针
#define NPROT_RCV_PKT_TO_LIST_ENTRY(_pPkt) (&((PNPROT_RECV_PACKET_RSVD)&((_pPkt)->ProtocolReserved[0]))->Link)

ndisprotQueueReceivePacket代码实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
VOID ndisprotQueueReceivePacket(IN PNDISPROT_OPEN_CONTEXT pOpenContext, IN PNDIS_PACKET pRcvPacket)
/*++
Routine Description:
Queue up a received packet on the open context structure. If the queue size goes beyond a water mark, discard a packet at the head of the queue.
Finally, run the queue service routine.
Arguments:
pOpenContext - pointer to open context
pRcvPacket - the received packet
Return Value:
None
--*/
{
PLIST_ENTRY pEnt;
PLIST_ENTRY pDiscardEnt;
PNDIS_PACKET pDiscardPkt;
do {
pEnt = NPROT_RCV_PKT_TO_LIST_ENTRY(pRcvPacket);
NPROT_REF_OPEN(pOpenContext); // queued rcv packet
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 如果处于活动的状态,并且正确的电源状态,那么就把这个包插入接收缓冲链表中。
if (NPROT_TEST_FLAGS(pOpenContext->Flags, NUIOO_BIND_FLAGS, NUIOO_BIND_ACTIVE) &&
(pOpenContext->PowerState == NetDeviceStateD0)) {
NPROT_INSERT_TAIL_LIST(&pOpenContext->RecvPktQueue, pEnt);
pOpenContext->RecvPktCount++;
DEBUGP(DL_VERY_LOUD, ("QueueReceivePacket: open %p, queued pkt %p, queue size %d\n", pOpenContext, pRcvPacket, pOpenContext->RecvPktCount));
}
else {
// 否则的话,就直接释放掉这个包即可。
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
ndisprotFreeReceivePacket(pOpenContext, pRcvPacket);
NPROT_DEREF_OPEN(pOpenContext); // dropped rcv packet - bad state
break;
};
// 如果输入缓冲区里包太多了,就要删除一个。
if (pOpenContext->RecvPktCount > MAX_RECV_QUEUE_SIZE) {
// 要删除的包的链节点指针
pDiscardEnt = pOpenContext->RecvPktQueue.Flink;
NPROT_REMOVE_ENTRY_LIST(pDiscardEnt);
// 接收包数量减去1
pOpenContext->RecvPktCount--;
// 可以释放锁了。
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 从链节点转换为包的指针
pDiscardPkt = NPROT_LIST_ENTRY_TO_RCV_PKT(pDiscardEnt);
// 把包释放掉。
ndisprotFreeReceivePacket(pOpenContext, pDiscardPkt);
// 打开上下文解引用。这是因为每入队一个都要增加一次引用计数。
NPROT_DEREF_OPEN(pOpenContext); // dropped rcv packet - queue too long
DEBUGP(DL_INFO, ("QueueReceivePacket: open %p queue too long, discarded pkt %p\n", pOpenContext, pDiscardPkt));
}
else
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
// 服务函数。这个函数看是否有未决的读请求。如果有,就取包来完成这个请求。
ndisprotServiceReads(pOpenContext);
} while (FALSE);
return;
};

上述ndisprotServiceReads负责接收数据包的出队和读请求的完成。该函数当读请求队列和接收包队列都不为空时,从包中取得数据,拷贝到IRP里,完成该IRP。pOpenContext->PendedReads为未决读请求队列,pOpenContext->RecvPktQueue为接收包缓冲队列,这俩实际是链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
VOID ndisprotServiceReads(IN PNDISPROT_OPEN_CONTEXT pOpenContext)
/*++
Routine Description:
Utility routine to copy received data into user buffers and complete READ IRPs.
Arguments:
pOpenContext - pointer to open context
Return Value:
None
--*/
{
PIRP pIrp = NULL;
PLIST_ENTRY pIrpEntry;
PNDIS_PACKET pRcvPacket;
PLIST_ENTRY pRcvPacketEntry;
PUCHAR pSrc, pDst;
ULONG BytesRemaining; // at pDst
PNDIS_BUFFER pNdisBuffer;
ULONG BytesAvailable;
BOOLEAN FoundPendingIrp;
DEBUGP(DL_VERY_LOUD, ("ServiceReads: open %p/%x\n", pOpenContext, pOpenContext->Flags));
NPROT_REF_OPEN(pOpenContext); // temp ref - service reads
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
// 只要读请求队列和接收包队列同时不为空,则可以做...
while (!NPROT_IS_LIST_EMPTY(&pOpenContext->PendedReads) && !NPROT_IS_LIST_EMPTY(&pOpenContext->RecvPktQueue)) {
FoundPendingIrp = FALSE;
// 获得第一个未决读请求
pIrpEntry = pOpenContext->PendedReads.Flink;
while (pIrpEntry != &pOpenContext->PendedReads) {
// 从链表节点得到IRP
pIrp = CONTAINING_RECORD(pIrpEntry, IRP, Tail.Overlay.ListEntry);
// 检查这个请求是否正在被取消。
if (IoSetCancelRoutine(pIrp, NULL)) {
// 把这个IRP出列。
NPROT_REMOVE_ENTRY_LIST(pIrpEntry);
FoundPendingIrp = TRUE;
break;
}
else {
// 如果是在去掉,则跳过这个IRP即可。使用下一个。
DEBUGP(DL_INFO, ("ServiceReads: open %p, skipping cancelled IRP %p\n", pOpenContext, pIrp));
pIrpEntry = pIrpEntry->Flink;
};
};
// 如果没有IRP,直接跳出结束。
if (FoundPendingIrp == FALSE)
break;
// 得到第一个包(最旧的),出队列。
pRcvPacketEntry = pOpenContext->RecvPktQueue.Flink;
NPROT_REMOVE_ENTRY_LIST(pRcvPacketEntry);
pOpenContext->RecvPktCount--;
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NPROT_DEREF_OPEN(pOpenContext);
// 从节点获得包。
pRcvPacket = NPROT_LIST_ENTRY_TO_RCV_PKT(pRcvPacketEntry);
// Copy as much data as possible from the receive packet to the IRP MDL.
// 得到IRP的输出缓冲地址。然后尽量拷贝更多的数据。
pDst = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
NPROT_ASSERT(pDst != NULL); // since it was already mapped
BytesRemaining = MmGetMdlByteCount(pIrp->MdlAddress);
pNdisBuffer = NDIS_PACKET_FIRST_NDIS_BUFFER(pRcvPacket);
// 请注意,每个PNDIS_BUFFER都是一个PMDL,同时PNDIS_BUFFER本身都是链表。用NdisGetNextBuffer可以从一个得到它的下面一个。包的数据实际上是保存在一个缓冲描述符链表里的。
while (BytesRemaining && (pNdisBuffer != NULL)) {
#ifndef WIN9X
NdisQueryBufferSafe(pNdisBuffer, &pSrc, &BytesAvailable, NormalPagePriority);
if (pSrc == NULL) {
DEBUGP(DL_FATAL, ("ServiceReads: Open %p, QueryBuffer failed for buffer %p\n", pOpenContext, pNdisBuffer));
break;
};
#else
NdisQueryBuffer(pNdisBuffer, &pSrc, &BytesAvailable);
#endif
// 如果还可以继续拷贝,就继续拷贝。
if (BytesAvailable) {
ULONG BytesToCopy = MIN(BytesAvailable, BytesRemaining);
NPROT_COPY_MEM(pDst, pSrc, BytesToCopy);
BytesRemaining -= BytesToCopy;
pDst += BytesToCopy;
};
NdisGetNextBuffer(pNdisBuffer, &pNdisBuffer); //得到下一个NDIS_BUFFER节点
};
// 拷贝好数据之后,结束IRP即可。
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = MmGetMdlByteCount(pIrp->MdlAddress) - BytesRemaining;
DEBUGP(DL_INFO, ("ServiceReads: Open %p, IRP %p completed with %d bytes\n", pOpenContext, pIrp, pIrp->IoStatus.Information));
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
// 如果这个包描述符不是从接收包池里分配的,那么就是从网卡驱动里重用的。如果是重用的,调用NdisReturnPackets归还给网卡驱动,让它释放。
if (NdisGetPoolFromPacket(pRcvPacket) != pOpenContext->RecvPacketPool)
NdisReturnPackets(&pRcvPacket, 1);
else
// 否则的话自己释放。
ndisprotFreeReceivePacket(pOpenContext, pRcvPacket);
NPROT_DEREF_OPEN(pOpenContext); // took out pended Read
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
pOpenContext->PendedReadCount--;
};
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NPROT_DEREF_OPEN(pOpenContext); // temp ref - service reads
return;
};