Getting Started with NetFilter
Introduction
The NetFilter component allows your application to intercept, inspect, modify, and block network packets in real time. You define a set of filter rules, start the filter, and then react to incoming packets through events or methods. This article focuses on how to use the NetFilter component's API.
Install and Uninstall the Drivers (Windows)
On Windows, the system driver must be installed on a machine before the NetFilter component can be used to intercept and process network traffic. This is done using the Install method, which is present in the NetFilter component API, as well as in a standalone Installer dynamic library (.dll), included with the toolkit. The NetFilter API method is useful when your application is responsible for installing the driver; the Installer dynamic library is designed to be used from installation scripts.
It may be necessary to reboot the system after calling Install to complete the driver installation process; the Install method will return a value that indicates whether or not this is necessary. Once the driver installation is complete, you will be able to use the NetFilter component to start filtering packets.
The driver needs to be installed only once. You do not need to install it each time you start your application. If the driver is not installed correctly, you will receive a "File not found" error when trying to use the NetFilter component, which means that the driver could not be found or loaded.
To uninstall the system driver, call the Uninstall method; available in both the NetFilter component API and the Installer dynamic library. This is not necessary when you install a new minor or major version of the driver.
Configuring the Component
The NetFilter component provides the ability to intercept, inspect, modify, and optionally block network packets at the driver level. Once initialized and started, it attaches to the network stack and delivers matching packets to the user code for processing according to defined filter rules.
Before using the NetFilter component, you may optionally configure the OperationMode property. This determines whether the component reports individual packets (via the Packet or NotifyPacket events) or collects aggregated packet statistics (via the Statistics event). For details on handling packets and collecting statistics, see the sections Packet Operations and Statistics and Monitoring.
Filter and passthrough rules can be added using the AddFilterRule and AddPassthroughRule methods (this is applicable when using the default operation mode omCapture). Filter rules determine which packets the component should process, while passthrough rules exclude packets from processing. For more details, see Adding Filter and Passthrough Rules.
Once the component is initialized and configured, call StartFilter to begin processing packets. To stop filtering, call StopFilter. Both methods can be used multiple times during the application's lifetime.
In summary, a typical workflow looks like this:
- Call Initialize.
- Optionally set OperationMode.
- List available adapters.
- Add any filter or passthrough rules.
- Call StartFilter.
Listing Adapters
Before defining any filter rules, monitoring the specifics of any particular adapters, or sending your own packets to any particular adapters, you may wish to check which adapters are available. To do so, call the ListAdapters method.
Doing so will populate the Adapters collection, and relevant details regarding each adapter (such as the type of interface, MAC address, etc.) are made available via this collection. Within this collection, the Id of each adapter may also be queried. The Id field of an adapter can be used in the following methods, to perform adapter-specific operations:
- AddFilterRule
- AddPassthroughRule
- GetStatistics
- InsertIncomingPacket
- SendPacket
- FlushSendBuffer
For example:
filter.ListAdapters();
if (filter.Adapters.Count > 0) {
Console.WriteLine("The following adapters are present in the system:");
foreach (Adapter adapter in filter.Adapters) {
Console.WriteLine("Name: {0}, ID: {1} - {2}", adapter.Name, adapter.Id, adapter.Description);
}
}
// Only capture packets from a specific adapter
filter.AddFilterRule(1, filter.Adapters[i].Id, Constants.PACKET_DIRECTION_ANY, -1, -1, "", Constants,RULE_ACTION_CAPTURE);
The Id of an adapter is also made available in the Packet, NotifyPacket, and Statistics events, indicating the adapter the packet or statistic is regarding.
Note that if any adapters are added or removed during the lifetime of the component, the AdapterAdded or AdapterRemoved events will fire (the Id of the new or removed adapter will be indicated via the AdapterId parameter in these events). In this case, various rules may be added or removed, though it is important to note that applications should ensure rule-related methods and properties are accessed in a thread-safe manner, as these methods and properties are not meant to be used from multiple threads at once.
Adding Filter and Passthrough Rules
The NetFilter component uses rules to determine which packets should be intercepted, modified, or allowed to pass through the network stack unaltered. Rules may be defined before calling StartFilter (though can be added or removed after the filter is started).
The component provides two types of rules, being filter rules and passthrough rules. Both types determine which packets should be processed by the component, but their purpose differs.
Filter Rules
Filter rules define which packets should be captured for processing by the component. When a packet matches a filter rule, depending on the rule's action:
- It triggers the Packet event (synchronous packet interception), or...
- It triggers the NotifyPacket event (asynchronous notification)
Filter rules are stored internally and can be accessed via the FilterRules collection. Filter rules can be added using the AddFilterRule method, and removed using either DeleteFilterRule or DeleteAllFilterRules.
Passthrough Rules
Passthrough rules define which packets should be excluded from being processed by other filter rules. If a packet matches a passthrough rule, the component will not process the packet and it will instead continue on through the stack.
These rules are useful when you want certain packet categories to be explicitly skipped by the specified filter rules, e.g., if you wish to exclude traffic you do not wish to modify.
Passthrough rules are stored internally and can be accessed via the PassthroughRules collection. Passthrough rules can be added using the AddPassthroughRule method, and removed using either DeletePassthroughRule or DeleteAllPassthroughRules.
Parameters for Rules
Both AddFilterRule and AddPassthroughRule share many of the same parameters. First, rules are specified with a RuleId, which is some application-assigned value that can be used later to identify the rule to remove from the mentioned collection, if necessary.
Next, rules are specified according to a particular AdapterId. If set, this specifies the adapter on which the driver should track and process packets. If this value is specified as -1, the rule applies to all adapters. For information on obtaining the Id's of adapters on the machine, please see the section above.
Whether the filter applies only to incoming or outgoing packets is determined by the Direction parameter. The component can capture incoming packets, outgoing packets, or both.
The MinPacketSize and MaxPacketSize parameters indicate the minumim and maximum size of packets to match to the relevant rule. If a packet is smaller or larger than what is specified by these parameters, the rule is not applicable to the packet. These parameters may be set to -1 to indicate that no lower or upper limit is set.
The BPFRule parameter specifies a BPF-compatible rule for matching the packet properties. The format of this rule is compatible with the libpcap filters. For additional reference, please refer to the libpcap documentation for the detailed guide. A BPF rule is optional, and an empty value may be passed when it is not needed.
Lastly, the Action parameter may be specified to indicate the action the driver should take in regard to the packet that matches the rule. Note this parameter is only applicable to filter rules, and not passthrough rules. Actions include either dropping (or discarding) the packet, capturing the packet with the possibility of modifying it in the Packet event, or (asynchronously) notifying that the packet was captured via the NotifyPacket parameter.
For example:
// Synchronous packet capture with the possibility to modify it in the event handler
filter.OnPacket += (o, e) => {
Console.WriteLine($"[{e.Timestamp}] Packet Captured.");
// Modify packet if necessary
};
filter.AddFilterRule(1, filter.Adapters[0].Id, Constants.PACKET_DIRECTION_ANY, -1, -1, "", Constants.RULE_ACTION_CAPTURE);
// Asynchronous packet capture (notification)
filter.OnNotifyPacket += (o, e) => {
Console.WriteLine($"[{e.Timestamp}] Packet Captured.");
};
filter.AddFilterRule(1, filter.Adapters[0].Id, Constants.PACKET_DIRECTION_ANY, -1, -1, "", Constants.RULE_ACTION_NOTIFY);
Packet Operations
The NetFilter component provides several ways to work with network packets once they have been captured. You can inspect and modify packets, send your own packets, or insert packets into the incoming packet stream. These capabilities allow full control over network traffic. Note this section is applicable when the OperationMode property is set to omCapture (default mode).
Modifying or Blocking Captured Packets
Assuming the appropriate filter rules are defined, captured packets are made available via the Packet event. This event is synchronous, allowing you to inspect and modify the packet before it continues through the network stack. You may modify any part of the packet, though it should be ensured that the changes maintain the correct protocol format to prevent any corruption.
For example, assuming we have added a filter rule to capture packets with the RULE_ACTION_CAPTURE action parameter:
// Example, zero out first four bytes of the packet payload
filter.OnPacket += (o, e) => {
try {
Console.WriteLine($"[{e.Timestamp}] Packet captured on Adapter {e.AdapterId}, Direction: {e.Direction}, Size: {e.PacketSize} bytes")
int payloadOffset = e.IPOffset;
int payloadSize = e.IPSize;
if (payloadSize > 0 && e.Buffer != IntPtr.Zero) {
byte[] packetData = new byte[e.BufferSize];
Marshal.Copy(e.Buffer, packetData, 0, e.BufferSize);
for (int i = 0; i < Math.Min(4, payloadSize); i++) {
packetData[payloadOffset + i] = 0;
}
Marshal.Copy(packetData, 0, e.Buffer, e.BufferSize);
// Indicate driver should send modified packet
e.Action = Constants.PACKET_ACTION_SEND_MODIFIED;
e.DataSize = e.BufferSize; // size of modified data
} else {
// Nothing modified, send packet unaltered
e.Action = Constants.PACKET_ACTION_SEND;
}
} catch (Exception ex) {
Console.WriteLine($"Error handling packet: {ex.Message}");
// Indicate failure
e.ResultCode = -1; // custom nonzero error code
e.Action = Constants.PACKET_ACTION_SEND; // optionally still send unmodified packet
}
}
Packets can also be dropped from continuing on in the network stack. For example, say we have added a filter rule with a direction parameter of PACKET_DIRECTION_OUTGOING and an action parameter of RULE_ACTION_CAPTURE. You can then inspect the packet data using the Buffer parameter, and parse out the destination IP and port, and set the Action parameter to RULE_ACTION_DROP.
// Example: block outgoing traffic to a specific IP or host
filter.OnPacket += (sender, e) =>
{
try
{
// Copy the packet buffer to a managed byte array
byte[] buf = new byte[e.BufferSize];
Marshal.Copy(e.Buffer, buf, 0, buf.Length);
// Here you would parse 'buf' to inspect the destination IP/port
// For demonstration purposes, let's assume we have a method:
// bool IsDestinationBlocked(byte[] packetData)
if (IsDestinationBlocked(buf))
{
Console.WriteLine($"Dropping packet to blocked destination on Adapter {e.AdapterId}");
e.Action = Constants.PACKET_ACTION_DROP; // drop the packet
}
else
{
// Let all other packets continue unmodified
e.Action = Constants.PACKET_ACTION_SEND; // default
}
e.ResultCode = 0; // indicate success
}
catch (Exception ex)
{
Console.WriteLine($"Error handling packet: {ex.Message}");
e.ResultCode = -1; // custom nonzero error code
e.Action = Constants.PACKET_ACTION_DROP; // fail-safe: drop packet
}
};
Sending Packets
The NetFilter component provides the SendPacket method for sending an outgoing packet over a specific adapter. Note that the caller is responsible for building complete packets suitable for sending via the specified adapter. This includes calculating checksums where it is required (IP, TCP, UDP, etc.) and inserting them into the packets being sent.
The SendPacket method can be used to either send a packet immediately, or insert a packet into the send buffer's queue. This is done so by specifying the BufferOnly parameter. Once a packet is queued, it can then be sent using the FlushSendBuffer method. This is particularly useful when sending a set of packets together, which is typically more efficient than sending packets one-by-one.
For example, to send a packet immediately:
byte[] packet = BuildPacket(); // custom method to build packet
int adapterId = filter.Adapters[0].Id;
filter.SendPacket(adapterId, packet, 1, false); // send 1 packet immediately
Or, to queue multiple packets in the buffer:
byte[] packet = BuildPacket();
filter.SendPacket(adapterId, packet, 1, true); // buffer 1 packet
filter.SendPacket(adapterId, packet, 5, true); // buffer 5 packets
// Later, send all buffered packets together
filter.FlushSendBuffer(adapterId);
Insert Incoming Packets
The InsertIncomingPacket method allows an application to inject a packet into the incoming packet flow of a network adapter. This is different from SendPacket, which sends packets out to the network. For example:
// Example: Inject a crafted packet as if it arrived from the network
byte[] incomingPacket = BuildPacket(); // Your custom method to create a packet
int adapterId = filter.Adapters[0].Id;
// Insert packet into incoming stream
filter.InsertIncomingPacket(adapterId, incomingPacket);
Note that the caller is responsible for building complete packets suitable for sending via the specified adapter. This includes calculating checksums where it is required (IP, TCP, UDP, etc.) and inserting them into the packets being sent.
Statistics and Monitoring
The NetFilter component supports two approaches for collecting network statistics:
- Statistics Mode - Enabled by setting the OperationMode property to 1 (omStat).
- On-Demand Statistics - Call GetStatistics manually (typically in a loop or timer) to retrieve statistics when needed, regardless of the current operation mode.
In both cases, the Statistics event will fire to report various details, but differ in how and when the statistics are produced.
Statistics Mode
In this mode, the component stops reporting per-packet events. Instead, the driver aggregates counters (bytes, packets, drops, etc.) and periodically delivers them through the Statistics event. First, the OperationMode property must be set to 1 (omStat).
By default, all statistics will then be periodically reported for all adapters. To control which adapters actually report statistics, you can disable the CollectStatistics field. After doing so and calling StartFilter, statistics will be reported via the Statistics event. Please see below for a simple example:
// Handle Statistics event
filter.OnStatistics += (o, e) => {
Console.WriteLine($"Adapter {e.AdapterId}, Received packets: {e.PacketsReceived}");
Console.WriteLine($"Adapter {e.AdapterId}, Received bytes: {e.BytesReceived}");
Console.WriteLine($"Adapter {e.AdapterId}, Sent packets: {e.PacketsSent}");
Console.WriteLine($"Adapter {e.AdapterId}, Sent bytes: {e.BytesSent}");
};
// Enable stats mode
filter.OperationMode = NetFilterOperationModes.omStat;
foreach (var adapter in filter.Adapters) {
if (!ShouldTrack(adapter.Id)) {
adapter.CollectStatistics = false;
}
}
filter.StartFilter(0);
Statistics mode is useful if you prefer continuous, event-driven updates, rather than manually polling statistics for specific network adapters.
Polling with GetStatistics
Alternatively, you can retrieve statistics manually, at any time, by calling the GetStatistics method. This is ideal if you want periodic or on-demand metrics for specific adapters, while the component is still capturing packet details (as the OperationMode is still set to 0 omCapture). As a simple example:
// Handle Statistics event
filter.OnStatistics += (o, e) => {
Console.WriteLine($"Adapter {e.AdapterId}, Received packets: {e.PacketsReceived}");
Console.WriteLine($"Adapter {e.AdapterId}, Received bytes: {e.BytesReceived}");
Console.WriteLine($"Adapter {e.AdapterId}, Sent packets: {e.PacketsSent}");
Console.WriteLine($"Adapter {e.AdapterId}, Sent bytes: {e.BytesSent}");
};
// Specify capture mode
filter.OperationMode = NetFilterOperationModes.omCapture; // default, but just showing for reference
filter.StartFilter(0);
// Some time later, check on statistics of particular network adapter
filter.GetStatistics(filter.Adapters[0].Id);
For additional information and specific examples, please refer to our documentation and demo applications in the PCAP Filter installation directory.
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@callback.com.