The Packet Capture Driver adds to Windows kernels the capability to capture raw packets from a network in a way very similar to UNIX kernels with BPF. In addition, it provides some functions, not available in the original BPF driver, to help the development of network test and monitor programs. Main goals of the packet capture driver are high capture performance, flexibility and compatibility with the original BPF for UNIX.
The result is a protocol driver that can:
The basic structure of the driver is shown in the next figure.
Figure 2.1: structure of the driver
The arrows toward the top of the picture represent the flow of packets from the network to the capture application. The bigger arrow between the kernel buffer and the application indicates that more than one packet can transit between these two entities in a single read system call. The arrows toward the bottom of the picture indicate the path of the packets from the application to the network. The thick arrow between the kernel buffer and the application indicates that, in particular situations, more than one packet can transit between these two entities in a single write system call. 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 a direct use of the PACKET.DLL dynamic link library.
The structure shown in Figure 2.1 (i.e. a single adapter and a single application) is a simplified description of the packet driver. The real structure 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.
Figure 2.2: structure of the driver with two adapters and a two applications
For each capture session established between an adapter and a capture program, the driver maintains 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 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. It is also possible to write an application that, interacting with the capture driver, is able to receive packets from more than one interface at the same time.
Notice that in versions older than 2.02, the only configuration possible in Windows 95 and Windows 98 was the one shown in Figure 2.1 (i.e. a single network adapter and a single capture application). This was due to limitations in the architecture of those versions. From version 2.02, in Windows 95/98 it is possible, like in Windows NT, to have more instances of the driver, and this allows to have more capture applications working at the same time. Furthermore, it is possible for a single application to work on more than one network adapter.
Core structure between the various Windows flavors is quite similar. Internal data structures are not very different, the packet buffer and filter are handled in the same way. The interaction with NDIS is very similar under the different platforms and is obtained by a set of callback functions exported by the driver and a group NDIS library functions (NdisTransferData, NdisSend...) used by the packet driver to communicate with the NIC driver. Differences between the different flavors concerns the interaction with the other parts of the operating system (read and write call handling from user-level applications, timer functions...), since the philosophy of the various operating systems is quite different.
The filter mechanism present in the packet capture driver derives directly from the BPF filter in UNIX, therefore all the things already said about the BPF filter are still valid. 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 pcap_compile function of libpcap) and pass it to the driver, and the filtering process will be done at kernel level. 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 of death. Therefore, without any 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 through it. For this reason, each 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 each incoming packet, discarding the ones that do not satisfy the filters conditions. If the packet satisfy the filter, it is copied to the application, or put in the buffer if the application is not ready to receive it. 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 thus minimizing the load on the system.
A very interesting 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 BPFs source code. This file contains three main functions: bpf_filter, bpf_filter_with_2_buffers and bpf_validate.
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 true 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 this method.
The packet driver supports timed reads. An application can set the read timeout through an IOCTL with code pBIOCSRTIMEOUT. If the timeout is different from 0, every read call performed by the application will return when the timeout expires even if no packets were received.
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 in the buffer with a header that maintains information like the timestamp and the size of the packet. Moreover, a padding is inserted between the packets in order to word-align them to increase the speed of the copies. 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.
Incoming packets are discarded by the driver if the buffer is full when a new packet arrives. The dimension of the drivers 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 at kernel level and is written explicitly 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, can 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 system call, the packets in the driver's buffer are copied to the application memory and the read call is immediately completed. More than one packet can be copied from the driver's circular buffer to the application with a single read call. This improves the performances because it minimizes the number of reads. Every read call in fact involves a context 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 BPF 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 buffers is high and the memory is used efficiently. When 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 buffer method uses the memory more efficiently with big buffers because the free memory is always available.
The packet driver handles the kernel-level and user-level packet buffers 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 whenever the application needs to do so. 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 even when it has different size from the circular buffer. This is important when the driver's buffers is big, because a large application buffer would be a waste of memory. 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, it can happen that the driver has to scan the headers of the packets in its buffer in order to determine the amount of bytes to copy. This process slows down a bit the capture process, therefore best performance are obtained when application and kernel buffers have the same size. 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. To avoid this problem, before the beginning of a copy the driver tries to determine the amount of data to copy without scanning the buffer. If this operation is successful, a small amount of CPU time is saved.
From version 2.01, a function (called PacketMoveMem) to perform the copies between the driver and the application was introduced. 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. In this way buffer's memory is used better and the loss probability is lower.
The packet capture driver allows to write raw data to the network. This can be useful to test either the network or the protocols and applications working on it. To send data, a user-level application performs a write system call (a write IOCTL call in Windows 95/98) on the packet driver's device file. The data is sent to the network as is, without encapsulating it in any protocol, therefore the application will have to build the various headers for each packet. The application does not need to generate the FCS because it is calculated by the network adapter hardware and it is attached automatically at the end of a packet before sending it to the network.
Notice that the sending rate of the packets to the network is not very high because of the need of a system call for each packet. Version 2.02 of the driver allows (on Windows NT/2000 only) to send a single packet more than one time with a single write system call. The user-level application can set, with an IOCTL call (code pBIOCSWRITEREP), the number of times a single packet will be sent to the network. For example, if this value is set to 1000, every raw packet written by the application on the driver's device file will be sent 1000 times. This feature can be used to generate high speed traffic because the overload of the context switches is no longer present, and is particularly useful to write tools to test networks, routers, and server programs. This optimization is made only in Windows NT and Windows 2000, while it is emulated at user level in PACKET.DLL in Windows 95/98. This means that an application that uses the multiple write method will run also in Windows 9x, but with a very low write speed compared to WindowsNTx.
An example of the use of the multiple write feature is the Traffic Generator (TG) application, whose source code can be found in the developer's pack.
In addition to the functions described in the previous paragraphs, the packet capture driver offers a pair of minor functions to grant full compatibility with original BPF:
A network manager is often not interested in the whole sequence of packets transiting on a network, but in statistical values about them. For example the user could be interested in network's utilization, broadcast level, but also in more refined information, like amount of mail traffic or number of web requests per second. 'Statistics' mode, or mode 1, is a particular working mode of the BPF capture driver that can be used to perform real time statistics on the network's traffic in an easy way and with the minimum impact on the system.
To put the driver in statistics mode, the user-level application makes an IOCTL call with code pBIOCSMODE, and the value '1' as input parameter. To set again the normal 'capture' working mode the same IOCTL, but with '0' as input parameter, can be called.
When in mode 1, the driver does not capture anything but limits itself to count the number of packets and the amount of bytes that satisfy the user-defined BPF filter. These values are passed to the application at regular intervals, whenever a timeout expires. The default value of this timeout is 1 second, but it can be set to any other value (with a 1 ms precision) with an IOCTL call (parameter pBIOCSRTIMEOUT). The counters are encapsulated in a bpf_hdr structure before being passed to the application. This allows to have microsecond-precise timestamps, and easy interaction with libpcap. libpcap is in fact able to decode the bpf_hdr structure, and pass the counters to the application.
Statistics mode is a powerful and versatile framework to calculate statistics on the network traffic. It is powerful because it requires very few system resources: only the BPF filter is applied to every packet, but no copy is performed. This ensures a low processor usage and avoids the need of any kernel buffer. It is versatile because it is based on the BPF filter machine. This allows the user to easily calculate complex statistics setting only the proper filter, and makes the use of this feature fully configurable from user-level.
To know more about how to use statistics mode, see the manual of PACKET.DLL. An example of the use of statistics mode is NetMeter, a simple monitor application available in the developer's pack.
Since Windows network interface drivers are shipped as binary files, the programmer does not have access to the network driver sources in order to insert any call to the Network Tap. The Network Tap in Windows is configured as a new protocol driver, so that NDIS threats it like IP or IPX protocols. In this way a single instance of a protocol stack is able to exploit different network technologies. Choosing NDIS made programming easier because the packet driver is able to use the same set of NDIS functions on all the platforms. The interaction with NDIS is however quite heavy, and this makes the packet capture driver (and above all the crucial 'tap' function) more complex than original BPF.
On the other hand, choosing to capture packets at protocol level allows the capture driver to work without any requirement from the network adapters driver, while original BPF requires that the adapters driver calls directly the tap function.
The location of the network tap influences the behavior of the capture driver. For example PPP (Point to Point Protocol), the most used protocol for managing serial links, uses some auxiliary protocols (LCP, Link Control Protocol, and NCP, Network Control Protocol) in order to configure and maintain the link up. These packets do not reach the NDIS upper layer because they are generated directly by the network link driver, therefore the Network Tap cannot capture them. Vice versa, UNIX tap is able to capture everything that is being transmitted on the wire because the tap call is inserted into the write and read functions of the network interface's driver.
BPFs in UNIX and Windows differ significantly in the management of the kernel and user packet buffers. As seen tn the introduction, BPF in Unix has the kernel buffer made by 2 blocks of memory, whose dimension is usually 4 KB. The main drawbacks of this architecture are the following:
To get over these limitations, our architecture makes use of a single dynamic circular buffer (default size 1 Mbytes). The increased size is possible thanks to the larger memories available nowadays and to the dynamic management of that buffer, which is allocated at the beginning of a capture and deallocated when the capture ends. This choice could make, in certain situations, less efficient copies from kernel to user space because of the impossibility to copy buffers with predetermined size, but it makes better use of memory.
Having a small buffer penalizes the original BPF architecture especially in case the application is not able to read as faster as the driver captures for a limited time interval. Such situation is common when data has to be transferred to disk (or even to screen) or when the network has bursty traffic. WinDump is expected to have a significantly better behavior than TCPdump in these conditions. Moreover WinDump is expected to be more affordable on slow machines in case it is deployed together with more complex (and slower) network analysis tools.
Another difference between UNIX and Windows is the mechanism to move packets from kernel to user space. In UNIX, packets captured by BPF are copied to the application either when the hold buffer is full or when a timeout expires, a technique that can be seen as a delayed write. In this way the application is awakened either when a certain number of packets has arrived or when a certain time interval has elapsed. This guarantees a low frequency of system calls and therefore a low processor usage, but it makes the capture application less responsive. Moreover this choice increases the risk of packets lost because there can be less free space into the kernel buffers (that are not freed as soon as possible) when a burst arrives.
A capture application in Windows is blocked only if the kernel buffer is empty; therefore it receives data as soon as the kernel buffer contains anything. If the application is very fast or if the frequency of incoming packets is not high, the amount of data present in the kernel buffer (and immediately passed to the application) could be very low. In this situation, the number of system calls used by the read process in Windows could be higher than in UNIX. This ensures a better use of the kernel buffer and a more responsive behavior of the capture application, but generates a higher processor load. Therefore, WinDump is expected to use more processor resources than TCPdump for low packets' frequencies, but approximately the same for high frequencies.
User buffer is implemented by Libpcap for UNIX as a fixed-size buffer with the same size of the two kernel buffers (usually 4Kb). Having both user and kernel buffer the same size, the copy process can be optimized. The entire kernel buffer is copied in a single read call, decreasing the number of system calls (i.e. of context switches between user and kernel mode).
In our architecture a user buffer with the same size of the kernel buffer could be a waste of memory because the kernel buffer can be very large. Therefore the user buffer can have any size, and usually is smaller than the kernel one (because kernel buffer is more important to avoid packet loss). If needed, the user can set a user buffer with the same dimension of the kernel buffer like in UNIX. This wastes more memory, but minimizes the overhead of the copy process and the number of system calls, allowing better performances. Default size of the user buffer is 256Kbytes.
The packet filter used by the packet driver under Windows is essentially the same used by BPF in UNIX. This means same processor and memory usage. bpf_filter_with_2_buffers is a bit slower, above all with long filtering programs, but is used only with very particular network interfaces.
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.
The versions of the packet capture driver for the various Windows flavors 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 means that functions provided by NDIS are exactly the same in the various Windows operating systems; 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 different drivers is quite similar, i.e. the data structure and the functions used are the same. However 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 operating systems (including the UNIX family) to perform operations different from the standard read and write (for example to set parameters or retrieve values) on a 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.
The following files are present and have the same meaning both in the Windows 95/98 and in the Windows NT/2000 drivers 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 drivers 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 BPF filter functions. |
BPF_FILTER.C | This file contains the BPF filters procedures. |
The following files are present only in the Windows 95/98 version:
File |
Description |
LOCK.C | This file contains the routines to lock the drivers 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 timers value. |
In this paragraph there is a simple description of the most important data structures of the driver. The structures will not be described in deep 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.
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 9x and in Windows NTx, 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 drivers are handled through this structure. |
IRP | This structure is used only by the NT/2000 versions of the driver. IRP, or I/O Request Packet, is the fundamental structure that the Windows NTx kernels use 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. IRP is a VERY complex structure that contains all the data needed to communicate with the user-level: addresses of buffers, process IDs... |
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. 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.
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 current 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 current 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 current instance had to drop from its opening. A packet is dropped if there is no more space to store it in the circular buffer that the driver associates to current instance. |
Bpfprogram | PUCHAR | The BPF filter program associated with current instance of the driver. |
StartTime | LARGE_INTEGER | This field contains a time reference used to convert the NTx and 95x system timers format into the BPF timestamps format. |
Buffer | PUCHAR | Pointer to the memory that contains the circular buffer associated with this drivers 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 of the non-fixed size of the captured packets, so the buffer can have an empty portion (too small for a packet) at its end. |
TimeOut | int | Read timeout in ms. Every read on current instance of the driver expires after the number of milliseconds hold in this variable is elapsed. |
ReadTimeoutTimer | KTIMER in
WinNTx UINT in Win9x |
The timer associated with the last read. Since the access to the driver is supposed to be synchronous, only a single read can be pending. This variable holds the kernel timeout associated to this read. Note that the format of this field in Windows NTx is different than Windows 9x, because kernel timers are handled in different ways. |
mode | int | The working mode of current instance of the driver. There are two possible values: 0 (normal capture mode) and 1 (statistics mode). |
Nbytes | __int64 | This field is used when current instance of the driver is in mode 1, to store the amount of bytes accepted by filter. |
Npackets | __int64 | This field is used when current instance of the driver is in mode 1, to store the number of packets accepted by filter. |
Nwrites | UINT | Contains the number of times a single write must be repeated. See The writing process section for more details. |
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 information 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 library function.
This structure contains information 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.
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. |
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.
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 times:
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 each 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 uses different mechanisms 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.
This function is called when a new instance of the driver is opened. It allocates and initializes variables and buffers needed by the new instance, 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 it 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. At this point the network adapter is opened here with a call to NdisOpenAdapter.
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.
This function is called when a running instance of the driver is closed. It stops the capture and buffering process and deallocates the memory buffers that were allocated by the PacketOpen function. The network adapter is closed here with a call to NdisCloseAdapter.
The corresponding callback function is PacketCloseAdapterComplete.
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.
The corresponding callback function is PacketResetComplete.
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.
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 on all the platforms.
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 (wirelen and buflen are different if the filter is invoked before the whole packet has arrived).
bpf_filter returns the number of bytes of the packet to be accepted. If the result is 0 the packet must be discarded. If the result is -1 the whole packet must be accepted.
See the paragraph on the filtering process for more details.
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 stored by the NIC driver in two different buffers. This can happen in Windows that does not specifies that the two buffers must be consecutives (even if usually it does). In the case of ATM LAN emulation, in which the Ethernet header is built by the software level and can be separate from data, this could not be verified. 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 needed, because it is noticeably slower than bpf_filter. It must in fact perform a check to determine the correct memory buffer every time the filter program needs to access to the packet data. For more details see the description of the Packet_tap function.
Once the packet capture driver is opened it has to be configured from 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 check the correctness of the filter. 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 current 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 current 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 the new size of the kernel buffer that is used by current instance of the driver. The new size 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 current instance of the driver. Values passed to the application are the Received and Dropped fields of the OPEN_INSTANCE structure associated with current instance. |
BIOCSMODE | Changes the working mode of current instance of the driver. This function sets the mode field of the OPEN_INSTANCE structure. The new working mode is received by the driver in a buffer associated with the IOCTL call. Mode 0 is normal capture mode and it is the default one. Mode 1 is statistics mode. Unless differently specified, in mode 1 a default 1000 ms timeout is set. |
BIOCSRTIMEOUT | Sets the
value of the read timeout in milliseconds. This function
sets the TimeOut field of the OPEN_INSTANCE
structure. The new timeout is received by the driver in a
buffer associated with the IOCTL call. The read timeout
has two different meanings depending if current instance
is in mode 0 or in mode 1.
|
BIOCSWRITEREP | Sets the number of times a single write call must be repeated. This function sets the Nwrites field of the OPEN_INSTANCE structure, and is used to implement the 'repeated write' feature of the driver. |
IOCTL_PROTOCOL_RESET | Resets the adapter associated with current instance of the driver. |
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 9x version:
Command |
Description |
IOCTL_OPEN | Called by the user-level applications when the driver is opened for a new capture. First, this function calls GetNDISAdapterName to retrieve the NDIS internal name of the adapter. In fact, in Windows 9x the name that NDIS uses for the an adapter is different from the adapter's device name. At this point PacketOpen is invoked to open the adapter and initialize it. |
IOCTL_CLOSE | Called by the user-level applications when the driver is closed after a capture. It calls the PacketClose function to close the adapter and free the resources allocated by current instance of the driver. |
IOCTL_PROTOCOL_READ | Called by user-level applications to read packets from the network. Calls the PacketRead function. |
IOCTL_PROTOCOL_WRITE | Called by user-level applications to write a packet to the network. Calls the PacketWrite function. |
IOCTL_PROTOCOL_MACNAME | Called by user-level applications to obtain the names of the NIC drivers on which the packet capture driver can work. Calls the PacketGetMacNameList function. |
This function is used to send packets to the network. The packet to send is passed to the driver with the WriteFile system call in Windows NTx, and with an IOCTL_PROTOCOL_WRITE IOCTL call in Windows 9x.
In Windows 9x, PacketWrite 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 the current driver's instance.
In Windows NTx, the behavior of this function is influenced by the Nwrites field of the OPEN_INSTANCE structure associated with current instance of the driver. If Nwrites is 1, the behavior is exactly the same of the Windows 9x PacketWrite. If Nwrites is greater than 1, it indicates the number of time the write must be repeated. In this case, PacketWrite sends sets of 100 packets before waiting any answer from the NIC driver, and repeats this behavior until all the packets are sent. This is done to speed up the generation process, that is fast enough to saturate a 10Mb Ethernet network (with 64 bytes packets) with a low level Pentium machine.
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.
This function is invoked when the user level application performs a ReadFile system call (in Windows NTx), or an IOCTL_PROTOCOL_WRITE IOCTL call (in Windows 95x). In both cases the application must provide a buffer that will be filled by the driver with the packets coming from the network. The behavior of this function is the same when the driver is in mode 0 and when it is in mode 1.
The first operation performed by PacketRead is a check on the driver's circular packet buffer associated with the current instance of the driver. There are two possible cases:
This function is used by all versions of the driver to copy data from the driver's circular buffer to the user-level application's buffer. It copies the data minimizing the accesses to the RAM. This is obtained aligning the memory accesses at the dword. 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 has been written to have a low overhead compared to a normal copy function. For this reason the head is updated no more often than every 1024 bytes.
This function is automatically invoked by the kernel when the timeout associated with a read call expires. First of all, the OPEN_INSTANCE structure associated with the current instance of the driver is found. From this structure ReadTimeout determines the working mode of current driver's instance. There are two possibilities.
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. PacketCancelRoutine removes the pending IRPs from the queue and completes them. This function was introduced to correct a bug of the old versions of the driver. Without this function, in fact, the driver hangs after the user-level application is closed until at least a packet arrives from the network. This is a Windows NT specific problem, so this function is present only in the Windows NT version of the driver.
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 )
Parameters are 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 maximum value allowed. |
LookaheadBufferSize | Size in bytes of the lookahead buffer. |
PacketSize | Size of the incoming packet, excluded the header. |
First of all, the ProtocolBindingContext parameter is used to determine if current instance is running in mode 0 or in mode 1.
Then, 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 the most obvious choice, because the packet arrives to the NIC driver through a single 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, there are two possibilities:
- The list of pending reads is empty. This means that the user-level application is not waiting for a packet in this moment. The incoming packet must be copied to the circular buffer of the packet capture driver. The address of the circular buffer and the pointers to head and tail are obtained from the OPEN_INSTANCE structure pointed by the ProtocolBindingContext input parameter. If the buffer is full (or, better, if the incoming packet does not fit in the remaining buffer space), the incoming packet is discarded. Packet_Tap allocates a NDIS_PACKET structure to receive the packet, and associates to this structure the correct memory position in the circular buffer. At this point, the amount of bytes specified previously by the filter is copied from the NIC driver's memory, and the bpf_hdr structure for this packet is built. Then the head and tail of the buffer are updated and Packet_Tap returns.
- The list of pending reads contains at least one element. This means that at the moment the user-level application is blocked waiting for the result of a read on the packet driver. Packet_Tap extracts the first pending read from the list and finds the NDIS_PACKET structure associated with it. This structure is used to receive the packet from the NIC driver. At this point, the amount of bytes specified previously by the filter is copied from the NIC driver's memory, and the bpf_hdr structure for this packet is built. Finally the application is awaked and Packet_Tap returns.
To build the bpf_hdr structure associated with a packet, 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.
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.
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 NDIS library is used to send the request to the NIC driver. This function is available only in Windows 95/98, because in Windows NT/2000 this operation is performed by the PacketIoControl function.
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 either by the PacketRequest or PacketIoControl function of the packet capture driver.
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 'space' character. The list ends with a \0 character. This function is present only in the Windows 95 version.
This assembler function 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.
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.