The NDIS Packet Capture Driver


1. Structure of the driver

The development of the NDIS Packet Driver started from the packet example provided with the Windows Device Driver Kit (DDK). It provides the basic structure of a network capture driver, useful as example and starting point, but has unacceptable performance limitations. Substantial changes had to be done to obtain a behavior good enough for a serious capture application. The result is a packet driver that can:

Notice that, as we said in the introduction, not all UNIX systems have BPF in the kernel. TcpDump can run on systems without BPF: on such systems it can work without a kernel buffer and can filter the packets at user level. This solution was adopted by the version 1.0 of the packets capture driver, that was very small and simple, but had very poor capture performances for the reasons already seen in the introduction.

Therefore, from version 2.0 the packet capture driver includes filtering and buffering capabilities like BPF.

 

1.1 Basic architecture of the various versions

The basic structure of the driver is shown in the next figure.

pic3.gif (4841 bytes)

                         Figure 2.1: structure of the driver

The arrows toward the top of the picture represent the flow of the packets from the network to the capture application. The bigger arrow between the buffer and the application indicates that more than one packet can transit between these two entities in a single call. The arrows toward the bottom of the picture indicate the path of the packets from the application to the network. WinDump and libpcap do not send packets to the network, therefore they use only the path from the bottom to the top. The driver however is not limited to the use with WinDump and can be used to create new network tools. For this reason it has been included the possibility to write packets, that can be exploited through the packet.dll API.

The structure shown in Figure 2.1 (i.e. a single adapter and a single application) is the only possible in Windows 95/98, because the Windows 95 version of the driver can have only a single running instance. This means that the user will not be allowed to run more than one capture program at the same time. This is due to limitations in the architecture of the current version. The Windows 95 version of the packet capture library in fact allocates a single global OPEN_INSTANCE, and this allows the driver to run no more than one instance at the same time (see section 2.3 for more details). 

The structure of the Windows NT version of the packet capture driver is more complex and can be seen in figure 2.2. This figure shows the driver's configuration with two network adapters and two capture applications.

pic4.gif (6430 byte)

Figure 2.2: structure of the driver with two adapters and a two applications

For every connection between an adapter and a capture program, the driver allocates a filter and a buffer. The single network interface can be used by more than one application at the same time. For example a user that wants to capture the IP and the UDP traffic of a network and save them in two separate files, can launch two sessions of WinDump on the same adapter (but with different filters) at the same time. The first session will set a filter for the IP packets (and a buffer to store them), and the second a filter for the UDP packets. In Windows NT it is also possible for a single application to receive packets from more than one interface, opening a different driver instance on each network adapter.

If we do not consider the differences just described, the Windows 95 and Windows NT versions are quite similar. The internal data structure are not very different, the buffer and the filter are handled in the same way. The interaction with NDIS is very similar in the two platform and is obtained by a set of callback functions exported by the driver and a couple of functions (NdisTransferData, NdisSend...) used to retrieve and send the packets. What is different between the two versions is the interaction with the operating system (handling of read and write calls of user-level applications, time functions...), since the philosophy of the two operating systems is quite different.

 

1.2 The filtering process

The filter mechanism present in the NDIS packet capture driver derives directly from the BPF filter in UNIX, therefore all the things said about the BPF filter are valid for the filter of the packet capture driver. An application that needs to set a filter on the incoming packets can build a standard BPF filter program (for example through a call to the libpcap’s pcap_compile function) and pass it to the driver, and the filtering process will be done in kernel mode. The BPF program is transferred to the driver through an IOCTL call with the control code set to pBIOCSETF. A very important thing is that the driver needs to be able to verify the application's filter code. In fact, as we said, the BPF pseudo-machines can execute arithmetic operations, branches, etc. A division by zero or a jump to a forbidden memory location, if done by a driver, bring inevitably to a blue screen. Therefore, without protection, a buggy or bogus filter program could easily crash the system. Since the packet capture driver can be used by any user, it could be easy for an ill-intentioned person to cause damages to the system throug it. For this reason, every filter program coming from an application is checked by the bpf_validate function of the driver before being accepted. If a filter is accepted, the driver stores the filter program and executes it on every incoming packet, discarding the ones that don’t satisfy the filter’s conditions. If the packet satisfy the filter, it is copied to the application, or put in the buffer if the application is not ready. If no filter is defined, the driver accepts all the incoming packets.

The filter is applied to a packet when it's still in the NIC driver's memory, without copying it to the packet capture driver. This allows to reject a packet before any copy, with the minimum load on the system.

A very nice feature of the BPF filter exploited by the packet capture driver is the use of a numeric return value. When a filter program is applied to a packet, the BPF pseudomachine tells not only if the packet must be transferred to the application, but also the length of the part of the packet to copy. This is very useful to optimize the capture process, because only the portion of packet needed by the application is copied.

The source code of the filter is in the file bpf_filter.c, and derives from the corresponding file of the BPF’s source code. This file contains three main functions: bpf_filter, bpf_filter_with_2_buffers and bpf_validate.

 

1.3 The buffering process

When an application wants to obtain the packets from the network, it performs a read call on the NDIS packet capture driver (this is not valid in Windows 95, where the application retrieves the data through a IOCTL call; however the result is the same). This call can be synchronous or asynchronous, because the driver offers both the possibilities. In the first case, the read call is blocking and the application is stopped until a packet arrives to the machine. In the second case, the application is not stopped and must check when the packet arrives. The usual and RECOMMENDED method to access the driver is the synchronous one, because of the difficulties in implementing the asynchronous one that can bring to errors. The asynchronous method can be used to do application-level buffering, but this is usually not needed because the buffering in the driver is more efficient and clean. WinDump uses the synchronous method, and all the considerations that we will make apply only to the synchronous method.

After an incoming packet is accepted by the filter, the driver can be in two different situations:

The driver uses a circular buffer to store the packets. A packet is stored int the buffer with a header that maintains informations like the timestamp and the size of the packet. Moreover, a padding is inserted between the packets in order to word-align them. The size of the buffer at the beginning of a capture is 0. It can be set or modified in every moment by the application through an IOCTL call. When a new dimension of the buffer is set, the packets currently in the buffer are lost. If the buffer is full when a new packet arrives, the incoming packet is discarded. The dimension of the driver’s buffer affects HEAVILY the performances of the capture process. In fact, it is likely that a capture application, that needs to make operations on each packet, sharing at the same time the processor with other tasks, will not be able to work at network speed during heavy traffic or bursts. This problem is more noticeable on slower machines. The driver, on the other hand, runs in kernel mode and is written expressly to capture the packets, so it is very fast and usually it does not loose packets. Therefore, an adequate buffer in the driver can store the packets while the application is busy, compensate the slowness of the application, and can avoid the loss of packets during bursts or high network activity. 

If the buffer is not empty when the application performs a read call, the packets in the buffer are copied to the application memory by the driver and the read call is immediately completed. More than one packet can be passed from the driver's circular buffer to the application with a single read call. This improves the performances because it minimizes the number of read. Every read call in fact involves a switch between the application and the driver, i.e. between user mode (ring 3 on Intel machines) and kernel mode (ring 0). Since these context switches are quite slow, decreasing their number means improving the capture speed. To maintain packet boundaries, the driver encapsulates the captured data from each packet with a header that includes a timestamp and length. The application must be able to properly unpack the incoming data. The data structure used to perform the encapsulation is the same used by BPF in the UNIX kernel, so the format of the data returned by the driver is the same returned by BPF in UNIX.

Notice that a single circular buffering method like the one of the packet capture driver, compared with a double buffering method, allows a better use of the memory. The double buffer method is suited when the buffers are very small (like in BPF): in this case the frequency of the swaps between the buffers is high and the memory is used efficiently. When the buffers become big, this method wastes a lot of memory and is not efficient, because the operations on a buffer can take a lot of time, impeding the use of the buffer's memory. The circular beffer method uses the memory more efficiently with big buffers because the free memory is always available.

The packet capture buffer handles the buffers for the packets in a very versatile way. It is possible to choose any dimension for the driver's circular buffer, limited only by the RAM of the machine. Furthermore, also the buffer used by the user-level application can have any size and can be changed in every moment. An important feature is that the two buffers are not forced to have the same size. The packet capture driver detects the dimension of the application's buffer, and fills it in the right way, also when it has a size different from its circular buffer. This is very important when the driver's buffers is big, because being obliged to set an application's buffer with the same size would be a waste of memory, or even an impossible thing. On the other hand, this mechanism has also a drawback: since the size of the packets is not fixed, when the dimension of the application's buffer is smaller than the number of bytes in the driver's buffer, the driver must scan the headers of the packets in its circular buffer in order to determine the amount of bytes to copy. This process slows a bit the capture process, therefore to obtain the best performances the application's buffer would have the same dimension of the driver's buffer. In fact, the driver detects when the dimension of the application's buffer is greater than the number of bytes in the driver's buffer, and when this is true, copies all the data without performing any scan.

From version 2.01, a function (called PacketMoveMem) was introduced to perform the copies between the driver and the application. This functions has an important feature: it updates the head of the circular buffer while copying. In this way the driver has not to wait the end of a copy process to use the memory freed by it. This means that the buffer is used better and the loss probability is lower.

 

1.4 Other functions

 

2. The source code of the driver

Warning: the following section of the documentation is quite complex, because it is intended for people that need to modify or upgrade the packet capture driver. If you don't need to do this and you are not interested in the driver's internals, you can skip this chapter.

2.1 introduction

The Windows 95/98 and the Windows NT versions of the packet capture driver are quite similar in the structure and in the organization of the source code. This derives from the fact that the interaction with the underlying levels of the network stack is handled using NDIS services and primitives, that are the same in the various operating systems of the Windows’ family. This involves that the functions provided by NDIS are exactly the same in the various Windows operating systems, and also the interaction with the network hardware is based on the same mechanisms.

What is different in the two versions is the interaction with the non-network parts of the operating system and with the user-level applications. In particular:

 

As we said, the structure of the two drivers is quite similar, i.e. the data structure and the functions used are the same, but for the reasons just reported the implementation of some functions and the use of some data structures can be very different. In the next chapters we will describe the most important functions and structures, trying to make a system-independent explanation of them.

Note: a IOCTL, (or IO Control) function, is the method used on most operanting systems (including the UNIX family) to perform operations different from the standard read and write (for example to set paramters or retrieve values from the driver). In Win32 an application performs an IOCTL call with the DeviceIoControl system call, passing as parameters the handle of the driver, the IOCTL code and the buffers used to send or receive data from the driver.

2.2 Organization of the source code

The following files are present and have the same meaning both in the Windows 95 and in the Windows NT driver’s source code:

File

Description

DEBUG.H This file contains the macros for used for the debug.
PACKET.H Contains the declarations of functions and data structures used by the driver.
PACKET.C This is the main driver’s file. It contains the DriverEntry function, which starts and initializes the driver, the functions to query the registry and the IOCTL handler function.
WRITE.C Contains the functions to send packets.
READ.C Contains the functions to read packets.
OPENCLOS.C Contains the functions to open or close an instance of the driver.
BPF.H Defines the data structures and the values used by the filter functions.
BPF_FILTER.C This file contains the BPF filter’s procedures.

The following files are present only in the Windows 95 version:

File

Description

LOCK.C This file contains the routines to lock the driver’s buffers.
REQUEST.C Contains the functions to perform OID query/set operations on the adapter.
NDISDEV.ASM This file contains the assembler function C_Device_Init, that is called by the operating system when the driver is loaded to begin the initialization process.
FUNCTS.ASM This file contains a couple of assembler functions used to call the services of the Virtual Timer Device to obtain the system timer’s value.

 

2.3 Data structures

In this paragraph there is a simple description of the most important data structures of the driver. The structures will not be described in every particular, because our goal is to give an overview of them and to point out the location of the most important driver's variables.

The main data structures of the packet capture driver are:

These structure are used by both the versions of the driver.

System structures used by the driver

The following is a list of the kernel and NDIS data structures mostly used by the driver. We provide only a short description of them and of their use in the driver. Detailed descriptions can be found in the manuals of NDIS and of Windows 95 and Windows NT DDKs.

Name

Description

NDIS_PACKET This NDIS structure defines the packet descriptors with chained buffer descriptors for which pointers are passed to many NdisXxx, MiniportXxx, and ProtocolXxx functions. A protocol driver that wants to write a packet to the network through the NDIS primitives must encapsulate it in a NDIS_PACKET structure. Similarly, a protocol driver receives a packet from the underlying NIC drivers in a NDIS_PACKET structure. Therefore, this structure is used heavily by the PacketRead and PacketWrite functions.
NDIS_PROTOCOL_CHARACTERISTICS This NDIS structure is used by the DriverEntry function during the initialization of the packet capture driver and contains all the information that NDIS will need to interact with the driver: the name of the protocol, the requested version of NDIS, the pointer to the various callback functions, and so on. This data is passed to NDIS through the NdisRegisterProtocol function.
LIST_ENTRY This is a kernel structure defined both in Windows 95 and in Windows NT, to support handling of doubly linked lists. The kernel provides a set of primitives to handle lists, insert and remove elements, and so on. To use these primitives the LIST_ENTRY data structure must be used. All the linked lists of the driver are handled through this structure.
IRP This structure is used only by the NT version of the driver. the IRP, or I/O Request Packet, is the fundamental structure that the Windows NT kernel uses for the communication between applications and drivers. All the data received by the driver from an application are packed in an IRP. All the data that the driver passes to the applications must be packed in an IRP. The IRP is a VERY complex structure that contains all the data needed to communicate with the user-level: addresses of buffers, process IDs...

 

The OPEN_INSTANCE structure

This structure is use by almost all the functions of the driver. It contains the variables and the data associated with a running instance of the driver. The OPEN_INSTANCE structure is enough to identify completely and univocally an instance of the driver. In Windows NT a new instance of the driver with its own OPEN_INSTANCE structure is associated to every application that performs CreateFile() system call on the packet capture driver. This means that more than one application can have access to the packet capture driver. In Windows 95 the OPEN_INSTANCE structure is a global variable and is unique in the driver. For this reason there is always only one running instance of the driver, and only an application can use the driver.

This structure has some differences in the two versions of the driver, because it has some system-dependent fields. However the most important members are present in both the versions.

The following are the main fields:

Variable

Datatype

Description

AdapterHandle NDIS_HANDLE The NDIS identifier of the adapter on which this instance is working.
PacketPool NDIS_HANDLE Pointer to a list of NDIS_PACKET structures (see the documentation of NDIS for more details about NDIS_PACKET) that the driver can use to receive or send packets to the adapter.
RcvList LIST_ENTRY Pointer to the list of pending reads that the driver must satisfy
RcvQSpinLock NDIS_SPIN_LOCK Spin lock used to protect the RcvList from simultaneous access by driver functions
Received Int Number of packets received by this instance from its opening, i.e. number of packet received by the network adapter since the beginning of the capture.
Dropped Int Number of packet that this instance had to drop from its opening. A packet is dropped if there is no more space in the buffer to store it.
Bpfprogram PUCHAR The BPF filter program associated with this instance of the driver.
StartTime LARGE_INTEGER This field contains a time reference used to convert the NT and 95 system timer’s format into the BPF timestamp’s format.
Buffer PUCHAR Pointer to the memory that contains the circular buffer associated with this driver’s instance. The packets captured from the interface and accepted by the filter are put in this buffer.
BufSize UINT Dimension in bytes of the circular buffer.
Bhead UINT Head of the circular buffer. This variable indicates the first valid byte of data in the circular buffer.
Btail UINT Tail of the circular buffer. This variable indicates the last valid byte of data in the circular buffer. Notice that this value can be smaller than Bhead, since the tail of a circular buffer can be below the head.
BlastByte UINT Pointer to the last byte of data in the memory allocated for the buffer. This variable is used to store the end of the buffer when Btail is smaller than Bhead. This is needed because the dimension of the captured packets is not fixed, so the buffer can have an empty portion (too small for a packet) at its end.

The INTERNAL_REQUEST structure

This structure is used by the driver to perform OID query and set operations on the network adapter through the underlying NIC driver. The query/set operations on the adapter can be done usually only by protocol drivers, but the packet capture driver allows the user-level applications to use this mechanism through an IOCTL function. The driver uses the INTERNAL_REQUEST structure to store the information on a query/set operation requested by the application. To know more about the OID requests see the documentation of PACKET.DLL.

The INTERNAL_REQUEST structure has an important field that is present both in the Windows 95 and in the Windows NT versions:

Variable

Datatype

Description

Request NDIS_REQUEST Contains all the data needed to perform a query/set operation on the adapter, like the pointers to the buffers for the informations and the number of bytes to read or write (see the NDIS documentation for a detailed description of the NDIS_REQUEST structure).

When the driver needs to read or set a parameter of the network adapter, it must build a proper NDIS_REQUEST structure and send it to the NIC driver through the NdisRequest NDIS function.

The DEVICE_EXTENSION structure

This structure contains informations on the packet capture driver, like the pointer to the DRIVER_OBJECT structure that describes the driver, or the ProtocolHandle that NDIS associates to the driver. It is used by the DriverEntry and PacketUnload functions to initialize or uninstall the driver.

The timeval structure

This structure is used to store the timestamp associated with a packet. It has two fields:

Variable

Datatype

Description

tv_sec long Holds the date of the capture in the standard UNIX time format (number of seconds from 1/1/1970).
tv_usec long Holds the microseconds of the capture.

BPF data structures

The packet capture driver uses for the interaction with user-mode applications a set of data structures originally created for BPF in UNIX. These structures are:

For a description of these structures see the documentation of the PACKET.DLL API.

 

2.4 Functions

This paragraph will describe the procedures of the packet capture driver. All the following functions are present in both the versions of the driver:

These functions are present only in the Windows 95 version of the driver:

The next function is present only in the Windows NT version of the driver:

Notice first of all that usually PacketXXX functions have a corresponding PacketXXXComplete procedure. This is due to the mechanism that NDIS defines for the interaction between NIC drivers and protocol drivers. This mechanism implies an asynchronous interaction, which takes place in two moments:

  1. The protocol driver asks a service to the NIC driver the proper function of the NDIS library. For example, the protocol driver can use the NdisSend function to send a packet to the network, the NdisRequest function to query or set a parameter of the adapter, and so on. These functions return always immediately, therefore the protocol driver is not blocked by them and can go on working.
  2. The NIC driver, after its work is concluded, communicates the end of the operation and sends the results invoking the callback function of the protocol driver associated with that kind of request.

A protocol driver has several callback functions to get the result of the various operations it can perform on the NIC diver. The names of these functions in the packet capture driver end with the word "Complete". Therefore PacketResetComplete is the callback function called by the NIC driver after the end of a reset operation of the adapter that was started by the PacketReset function, PacketSendComplete is called after the conclusion of a send operation started by the PacketWrite function with a call to NdisSend, etc.

The most important callback function of the packet capture driver is Packet_tap. It is called by the NIC driver every time a new packet arrives from the network, to transfer the packet to the capture driver. This function does the main work in the packet capture driver.

The interaction with NDIS and with the NIC drivers is very similar in Windows 95/98 and in Windows NT, because NDIS is a system independent architecture that provides the same interface to the protocol drivers written for all the Win32 platforms. This means that the section of the packet capture driver that interacts with the underlying levels of the network stack is quite similar in the Windows 95 and Windows NT versions.

The interaction with the upper levels, i.e. with the user-level applications, is instead quite different in the two versions. The reason of this is that the interaction between an application and a driver in Windows 95 is different than in Windows NT. In Windows NT the application opens and initializes instances of the packet capture driver, reads packets, writes packets, performs IOCTL calls respectively with the CreateFile, ReadFile, WriteFile and DeviceIoControl system calls. The driver has to manage various types of requests from the application. In Windows 95 all the interaction between the packet capture driver and the applications (included open, close, read and write operations) is done through IOCTL calls using the DeviceIoControl system call, therefore the driver has to manage only this type of calls.

 

DriverEntry

This procedure is called by the operating system when the packet capture driver is loaded and started. It makes all the initializations needed by the driver. In particular this function allocates a NDIS_PROTOCOL_CHARACTERISTICS structure and initializes it with the protocol data (version, name, etc.) and the addresses of all the callback functions of the driver. This structure is then passed to NDIS with a call to NdisRegisterProtocol. The Windows NT version of the driver calls also the IoCreateDevice function to pass to the operating system the addresses of the handlers for the open/close, read, write and IOCTL requests.

PacketOpen

This function is called when a new instance of the driver is opened. It allocates and initializes the variables and buffers needed by the new instance, and fills the OPEN_INSTANCE structure associated with it and opens the adapter. The time constants used to obtain the timestamps of the packets during the capture are initialized here. This initialization is really brain damage, because requires the conversion of dates from the internal format of Windows NT (100-nanosecond intervals since January 1, 1601) and Windows 95 (milliseconds since January 1, 1980) to the UNIX one (seconds since January 1, 1970), used by the applications. In Windows NT the network adapter is opened here with a call to NdisOpenAdapter. In Windows 95 this operation is done during the driver's startup process.

PacketOpenAdapterComplete

Callback function associated with the NdisOpenAdapter function of NDIS library. It is invoked by NDIS when the NIC driver has finished an open operation that was started previously by the PacketOpen function with a call to NdisOpenAdapter.

PacketClose

This function is called when a new instance of the driver is opened. It stops the capture and buffering process and deallocates the memory buffers that were allocated by the PacketOpen function. In Windows NT the network adapter is closed here with a call to NdisCloseAdapter. In Windows 95 this operation is done when the driver is unloaded.

PacketCloseAdapterComplete

Callback function associated with the NdisCloseAdapter function of NDIS library. It is invoked by NDIS when the NIC driver has finished an open operation that was started previously by the PacketClose function with a call to NdisCloseAdapter.

PacketReset

Resets the adapter associated with the current instance, calling the NdisReset function of NDIS library. This function is defined only in Windows 95, because the Windows NT version of the driver calls NdisReset directly from the PacketIoControl function.

PacketResetComplete

Callback function associated with the NdisReset function of NDIS library. It is invoked by NDIS when the NIC driver has finished an open operation that was started previously by the packet capture drivers with a call to NdisCloseAdapter.

PacketUnload

This function is called by the system when the packet capture driver is unloaded. It frees the DEVICE_EXTENSION internal structure, and calls NdisDeregisterProtocol to deregister from NDIS.

bpf_validate

This function checks a new filter program, returning true if it is a valid program. See the paragraph on the filtering process for more details. This function is exactly the same in Windows 95 and in Windows NT.

bpf_filter

The filtering function that is normally used by the capture driver. The syntax of this function is:

u_int bpf_filter(register struct bpf_insn *pc,
                 register u_char *p,
                 u_int wirelen,
                 register u_int buflen)

pc points to the first instruction of the BPF program to be executed, p is a pointer to the packet on which the filter is applied, wirelen is the original length of the packet, and buflen is the current  length of the packet.

bpf_filter returns the number of bytes of the packet to be accepted. If the result is 0 the packet must be dicarded. If the result is 1 the whole packet must be accepted.

See the paragraph on the filtering process for more details. Like bpf_validate, also this function is exactly the same in Windows 95 and in Windows NT.

bpf_filter_with_2_buffers

The alternative filtering function, with two input data buffer. The syntax of this function is:

u_int bpf_filter_with_2_buffers(register struct bpf_insn *pc,
                                register u_char *p,
                                register u_char *pd,
                                register int headersize,
                                u_int wirelen,
                                register u_int buflen)

pd is the pointer to the beginning of packet's data. headersize is the size of the header. All the remaining parameters, and the return values are the same of the bpf_filter function.

This function is used by the Packet_Tap procedure only if the packet's header and data are store in the NIC driver's memory in two different buffers. This is a situation that can never happen in UNIX, but is possible in Windows, for example in the case of ATM LAN emulation, in which the Ethernet header is built up by the software level and can be separate from data. In such a case, a filtering function like bpf_filter_with_2_buffers is needed by the packet driver. bpf_filter_with_2_buffers is not the default filtering function, and is used only if necessary, because it is noticeably slower than bpf_filter. It must in fact perform a check to determine the correct buffer for each memory access of the filter program. For more details see the description of the Packet_tap function.

PacketIoControl

Once the packet capture driver is opened, it is configured by user-level applications with IOCTL commands, using the DeviceIoControl system call. This function Handles IOCTL calls from the user level applications. This function is particularly important in the Windows 95/98 version of the driver, where it manages read and write requests.

Once the code of the command is obtained, a switch case determines the operation to be performed.

Next table summarizes the main IOCTL operations used in both the versions of the driver.

Command

Description

BIOCSETF This function is used by an application to set a new BPF filter in the driver. The filter is received by the driver in a buffer associated with the IOCTL call. Before allocating any memory for the new filter, the bpf_validate function is called to see if it is valid. If this function returns TRUE, the filter is copied to the driver's memory, its address is stored in the bpfprogram field of the OPEN_INSTANCE structure associated with this instance of the driver, and the filter will be applied to all the incoming packets. Before returning, the function empties the circular buffer used by this instance to store packets. This is done to avoid the presence in the buffer of packets that do not match the filter.
BIOCSETBUFFERSIZE This function is used by an application to set a new dimension of the buffer, that is used by this instance of the driver to store the incoming packets. The new dimension is received by the driver in a buffer associated with the IOCTL call. This function deallocates the old buffer, allocates the new one, and resets all the parameters associated with the buffer in the OPEN_INSTANCE structure. Since the old buffer is deallocated, all the currently buffered packets are lost.
BIOCGSTATS Returns to the application the number of packets received and the number of packets dropped by this instance of the driver. The values passed to the application are the Received and Dropped fields of the OPEN_INSTANCE structure associated with this instance.
IOCTL_PROTOCOL_RESET Resets the adapter associated with this instance of the driver. In Windows 95 the PacketReset driver's function is called. In Windows NT the NdisReset function of the NDIS library is called directly from here. This is the reason why the Windows NT version has not a PacketReset function.
IOCTL_PROTOCOL_SET_OID This call is used to perform a OID set operation on the adapter's NIC driver. The code of the operation and the parameters are received by the driver in a buffer associated with the IOCTL call. The result is returned to the application without changes.
IOCTL_PROTOCOL_QUERY_OID This call is used to perform a OID query operation on the adapter's NIC driver. The code of the operation and the parameters are received by the driver in a buffer associated with the IOCTL call. The result is returned to the application without changes.

The following IOCTL operations are defined only in the Windows 95 version:

Command

Description

IOCTL_OPEN Called by the user-level applications when the driver is opened for a new capture. First of all this function checks the number of running instances: if there is already another running instance, the open operation fails, because the Windows 95 version accepts only a running instance at a time. If no other running instance is detected, the OPEN_INSTANCE structure is set and the PacketOpen function is called.
IOCTL_CLOSE Called by the user-level applications when the driver is closed after a capture. It calls the PacketClose function and releases the driver, allowing its use to other applications.
IOCTL_PROTOCOL_READ Called by the user-level applications to read packets from the network. Calls the PacketRead function.
IOCTL_PROTOCOL_WRITE Called by the user-level applications to write a packet to the network. Calls the PacketWrite function.
IOCTL_PROTOCOL_MACNAME Called by the user-level applications to obtain the names of the NIC drivers on which the packet capture driver can work. Calls the PacketGetMacNameList function.

PacketWrite

This function sends a packet to the network. The packet to send is passed to the driver with the WriteFile system call in Windows NT, and with an IOCTL_PROTOCOL_WRITE IOCTL call in Windows 95. This function allocates a NDIS_PACKET structure and associates to it the data received from the application. Then The NdisSend function from the NDIS library is used to send the packet to the network through the adapter associated with this driver's instance.

PacketSendComplete

Callback function associated with the NdisSend function of NDIS library. It is invoked by NDIS when the NIC driver has finished to send a packet to the network, after PacketWrite was called. This function frees the NDIS_PACKET structure that was allocated in the PacketWrite function, and awakens the application from the WriteFile or DeviceIoControl system call.

PacketRead

This function is invoked when the user level application performs a ReadFile system call (in Windows NT), or an IOCTL_PROTOCOL_WRITE IOCTL call (in Windows 95). In each of the two cases the application must provide a buffer that the driver will fill with the packets coming from the network.

The first operation performed by PacketRead is a check on the circular packet buffer associated with this instance of the driver. There are two possible cases:

  1. The packet buffer is empty. The application's request cannot be satisfied immediately, and the system call must be blocked until at least a packet arrives from the net. After a NDIS_PACKET structure is allocated, the buffer received from the application is mapped in the driver's address space and associated with this structure. The NDIS_PACKET structure is put in the linked list of pending reads. It will be extracted from this list by the Packet_Tap function when a new packet will be captured. At this point PacketRead returns, but without awaking the application.
  2. The packet buffer contains data: the application's request can be satisfied immediately. Firstly, the driver obtains the length of the buffer passed by the application, to which the packets will be copied. Knowing this value, PacketRead tries to copy all the data to the application without further operations. This is possible if the number of bytes present in the driver's circular buffer is smaller than the size of the buffer passed by the application. If the application's buffer is too small to contain all the data present in the driver's packet buffer, a scan is performed to determine the amount of bytes to transfer. After the scan, NdisMoveMemory is invoked to copy the packets. At this point the application is awaked and PacketRead returns.

PacketMoveMem

This function is used by all the versions of the driver to copy the memory from the driver's circular buffer to the user-level application's buffer. It copies the data minimizing the accesses to the RAM. Furthermore, it updates the head of the circular buffer every 1024 bytes copied. In this way the circular buffer is updated during the copy, allowing a better use of the circular buffer and a lower loss probability. The function is write to have a low overhead compared to a normal copy function. For this reason the head is updated every 1024 bytes and not after every copy.

PacketCancelRoutine

This is the cancel routine set in the PacketRead function with a call to IoSetCancelRoutine. It is called by the operating system when the read system call is cancelled, for example when the user-level application is closed during a read on the packet capture driver. It removes the pending IRPs from the queue and will complete them. Without this function, the driver hangs the user-level application after the end of the capture process until a packet passes the filter. This is a Windows NT specific problem, so this function is present only in the Windows NT version of the driver.

Packet_tap

Packet_tap is invoked by the underlying NIC driver when a packet arrives to the network adapter. In Windows 95 and in Windows NT it has the same following syntax:

NDIS_STATUS Packet_tap ( NDIS_HANDLE ProtocolBindingContext,
                         NDIS_HANDLE MacReceiveContext,
                         PVOID HeaderBuffer,
                         UINT HeaderBufferSize,
                         PVOID LookAheadBuffer,
                         UINT LookaheadBufferSize,
                         UINT PacketSize )

the meaning of the parameters is the following:

Parameter

Description

ProtocolBindingContext Pointer to a OPEN_INSTANCE structure that identifies the instance of the packet capture driver to which the packets are destined.
MacReceiveContext Handle that identifies the underlying NIC driver that generated the request. This value must be used when the packet is transferred from the NIC driver as a parameter for the NdisTransferData NDIS call.
HeaderBuffer Pointer to the buffer in the NIC driver's memory that contains the header of the packet.
HeaderBufferSize Size in bytes of the header buffer.
LookAheadBuffer Pointer to the buffer in the NIC driver's memory that contains the incoming packet's data. During initialization, the packet capture driver performs a OID call that tells to the NIC driver to force the dimension of this buffer to the maximun value allowed.
LookaheadBufferSize Size in bytes of the lookahead buffer.
PacketSize Size of the incoming packet, excluded the header.

First of all Packet_Tap executes the BPF filter on the packet. The filter is obtained from the OPEN_INSTANCE structure pointed by the ProtocolBindingContext input parameter. To optimize the capture performances and minimize the number of bytes copied by the system, the BPF filter is applied to a packet before copying it, i.e. when it is still in the NIC driver's memory.

Notice that the capture driver receives the incoming packet from NDIS in two buffers: one containing the header and one containing the data. The reason of this subdivision is that normally a protocol driver makes separate uses of the header  (used to decode the packet), and the data (sent to the applications). This is not the case of the packet capture driver, that works at link-layer level and needs to threat the whole packet as a unit. However, we noted that in the great part of the cases the packet is stored in the NIC driver's memory in a single buffer (this is natural, because the packet arrives to the NIC driver through a single DMA transfer). In these situations HeaderBuffer and LookAheadBuffer point to two different sections of the same memory buffer, and it is possible to use HeaderBuffer as a pointer to the whole packet and use the standard bpf_filter function. The packet driver in every case performs a check on the distance between the two buffers: if it is equal to HeaderBufferSize (i.e. there is a single buffer), the standard bpf_filter function is called, otherwise bpf_filter_with_2_buffers is called.

If the filter accepts the packet, Packet_Tap tries to extract an element from the linked list of pending reads. This operation can have two results:

To build the bpf_hdr structure associated with a packet, the current value of the microsecond timer must be obtained from the system. In Windows NT this is done by means of a call to the kernel function KeQueryPerformanceCounter, in Windows 95 with a call to the packet driver's function QuerySystemTime. Since Packet_Tap is called directly by the NIC driver, the receive timestamp is closer to the actual reception time.

PacketTransferDataComplete

This function is called by Packet_Tap when the packet must be passed directly to the user-level application. PacketTransferDataComplete releases the NDIS_PACKET structure and the buffers associated with the packet and awakes the application.

PacketRequest

This function is used to send a OID query/set request to the underlying NIC driver. PacketRequest allocates a INTERNAL_REQUEST structure and fills it with the data received from the application. Then The NdisRequest function from the NDIS library is used to send the request to the NIC driver. This function is present only in Windows 95, because in Windows NT this operation is performed by the PacketIoControl function.

PacketRequestComplete

Callback function associated with the NdisRequest function of NDIS library. It is invoked by NDIS when the NIC driver has finished an open operation that was started previously by the PacketRequest or PacketIoControl  function of the packet capture driver.

PacketGetMacNameList

This function returns the names of all the NIC drivers to which the driver is attached. It is invoked by the PacketIoControl function when a IOCTL_PROTOCOL_MACNAME command has been received. The names are separated by a \0 character. The list ends with a double \0 character. This function is present only in the Windows 95 version.

GetDate

This assembler functions returns the current system date as a 64 bit integer. The low word contains the current time in milliseconds. The high word contains the number of days since January 1, 1980. This function is present only in the Windows 95 version of the driver.

QuerySystemTime

This assembler functions returns the current microsecond system time as a 64 bit integer. This function is present only in the Windows 95 version of the driver, where it is used to get the timestamps of the packets.

 

3. Performances

Writing a capture driver, good performances are the main goal to reach. With 'good performances' we mean low use of system resources (memory and processor) in order to leave them for the user-level monitors and analyzers, but also low  probability to loose packets (also with fast networks or relatively slow machines).

The following main parameters influence the performances of the capture process: the efficiency of the filter, size of the packet buffer, the number of bytes copied, and the number of system call that needs to be execute by the application.

  1. The efficiency of the packet filter is a very important parameter, because the filter must be applied to every incoming packet (i.e. thousands of times per second). The packet capture driver uses the fast and highly optimized BPF filter (for more details about the performances of BPF filter, see [McCanne and Jacobson 1993]), whose virtual-processor architecture is suited for modern computers architectures. Notice that, due to the architecture of NDIS, the packet capture driver has two different filtering functions: bpf_filter and bpf_filter_with_2_buffers. The first is faster, while the second is more general. The packet capture driver, when possible, always uses the first function, that allows better performances.
  2. The size of the buffer, as already said, is parameter that influences the number of packet loss during a capture. On the same machine, a bigger buffer means lower loss probability. Since the correct dimension of the buffer is a very subjective parameter, and depends on various factors, like network speed or machine characteristics, the packet capture driver offers a dynamic buffer, that can be set to any dimension in every moment. In this way it is possible to set very big buffers on machines with an huge amount of RAM. Notice however that the buffer is deallocated when the driver's instance is closed, therefore the memory is used by the driver only during the capture process (i.e. when really needed).
  3. The number of bytes copied by the system is perhaps the most important parameter when talking about performances. If not enough care is put in the optimization of the copies, this task can absorb a lot of processor time. First of all, the packet capture driver applies the filter to an incoming packet as soon as it arrives to the system: the packet is filtered when it is still in the NIC driver's memory, without copying it. This means that no copy is needed to filter the packet. The filter tells how many bytes of the packets are needed by the user-level application (for example WinDump needs only the first 68 bytes of each packet). The packet capture driver copies only this amount of bytes (and not the whole packet) to the circular buffer. This is very important also because reduces the space occupied by the packet in the circular buffer, that is used more efficiently. The selected packet is then copied to the user-level application during a read system call. Summarizing, there are two copies of the cut packet, none of the entire packet, that is equivalent of the number of copies done by the UNIX version.
  4. Every read system call implies a switch of the operating system from user-mode (ring 3) to kernel-mode (ring 0), and another to return to user-mode. This process is notoriously slow and can influence the performances of the capture. Since a user-level application might want to look at every packet on the network and the time between packets can be only a few microseconds, it is not possible to do a read system call per packet. The packet capture driver collects the data from several packets and copies it to the application's buffers in a single read call. The number of packets copied is not fixed and depends on the dimension of the application's buffer that will receive the packets: the driver detects the size of this buffer, and copies packets to it until it's full. Therefore, it is possible to decrease the number of system calls increasing the dimension of the application's read buffer.