This chapter introduces the main concepts of libnetdude, explains how the features are implemented, and how the code is used. The next chapter gives a few examples.
![]() | Before you can do anything with it, you need to initialize libnetdude with a call to libnd_init(). |
Trace files are libnetdude's bread and butter. They are represented by instances of LND_Trace, which maintain state for each trace file the user is manipulating. State information consists of consistency management data, filter settings, packet iteration configuration, modification status and more. See libnd_trace.h for details.
Instantiating a trace structure is done using libnd_trace_new(), passing it the path of the trace file to load or NULL when you want to create a new file. Saving is done using libnd_trace_save() and libnd_trace_save_as(), and releasing a trace is done using libnd_trace_free().
When you open a trace file, libnetdude does not load any packets. If you want to load any packets, from what part of the file, and how many packets, is entirely up to you. This is explained in detail in the section on trace parts below.
To allow applications to integrate libnetdude well, you can register trace observers. These observers will then receive notifications when certain events occur on a trace file. A good example are GUI-based applications — these can register an observer to update the GUI whenever the trace is modified, for example. Trace observers are created using libnd_trace_observer_new(). The different possible events are defined in the LND-TraceObserverOp enumeration. The actual observer structures contain function pointers to the various event callbacks. These pointers are initialized to safe defaults when a new observer is created. You then hook different implementations into this structure, and register it with a trace using libnd_trace_add_observer(). Events are pushed to the observers using libnd_trace_tell_observers().
To associate arbitrary data items with a trace, every trace comes with a simple key/value-based data storage, accessible through libnd_trace_get_data(), libnd_trace_set_data(), and libnd_trace_del_data(). libnetdude only stores and retrieves the data objects — you have to do any cleaning-up yourself before releasing a trace.
Protocols make packet handling easy in libnetdude. Each protocol encodes knowledge of the protocol structure found in a packet, such as the protocol header, protocol data length etc. The more protocols libnetdude can access, the more detailed the interpretation of the raw packet data in packets becomes. In libnetdude, protocol knowledge is provided in protocol plugins, to allow support for new protocols to be added in a modular fashion.
By default, libnetdude ships with protocol plugins for ARP, Ethernet, FDDI, ICMP, IP, Linux SLL, SNAP headers, TCP, and UDP. Everything else is interpreted as raw protocol data. More protocol plugins are always welcome! For details, see the chapter on how to write a protocol plugin below.
Protocol plugins get picked up, initialized and registered automatically when libnetdude is bootstrapped. Each protocol is registered in the protocol registry, from which you can retrieve protocols by providing the layer in the protocol stack that the protocol resides in (as defined through the LND_ProtocolLayer enumeration), and the magic value that the protocol is commonly known under. At the linklevel, these are the DLT_xxx values as defined in /usr/include/net/bpf.h, at the network layer the ETHERTYPE_xxx values of /usr/include/net/ethernet.h, at the transport layer the IPPROTO_xxx values of /usr/include/net/in.h, and at the application level, these are the well-known port numbers as found in /etc/services. We'll look at an example of how to use this registry once we have introduced libnetdude's packets.
Packets are instances of type LND_Packet. When requested, libnetdude initializes packets so that they contain detailed information about the nesting of the protocols contained in them. This depends on the number of protocols available to the library. libnetdude's packets also contain the packet data and packet header as provided by libpcap.
You normally do not need to create and initialize packets manually. Typically, you will either iterate over the packets in a trace area and obtain the initialized current packet from a packet iterator, or you will ask libnetdude to load a number of packets into memory for you and then directly operate in that sequence of loaded and initialized packets.
Initialized packets make it easy to access protocol information of a given protocol at a given nesting level. You can easily obtain the first nested IP header from a packet, for example. Forget writing your own protocol-demuxer from now on.
Similarly to traces, applications can register observers to be informed when certain operations are performed on a packet.
Now that protocols and packets are introduced, let's look at some code. Let's assume that we want to obtain the TCP header of a packet. We first retrieve the TCP protocol from the protocol registry.
#include <libnd.h> #include <netinet/in.h> #include <netinet/tcp.h> LND_Trace *trace; LND_Packet *packet; LND_Protocol *tcp; struct tcphdr *tcphdr; libnd_init(); /* Open a tracefile, load a few packets ... */ if (! (trace = libnd_trace_new("foo.trace"))) { /* Didn't work -- appropriate error handling. */ } libnd_tpm_load_packets(trace->tpm); /* Obtain the packet somehow: */ packet = libnd_trace_get_packets(trace); if (! (tcp = libnd_proto_registry_find(LND_PROTO_LAYER_TRANS, IPPROTO_TCP))) { /* Protocol not found -- handle accordingly. */ } |
if (! (tcphdr = (struct tcphdr *) libnd_packet_get_data(packet, tcp, 0))) { /* This packet does not contain TCP headers */ } |
As mentioned above, when opening a trace file libnetdude does not
load the packets contained in the file into memory, because this
approach is clearly bound to fail for larger traces -- and tcpdump
traces often are hundreds of megabytes in size.
It can load up to a configurable number of packets from a specified
point in the file upon request, but typically you will just iterate
over packets in a certain trace area, loading
them one at a time to perform some operation on them. In code,
trace areas are instances of
Iterating over the packets in a trace area and modifying them creates a trace part (in code: instances of type LND_TracePart) — a temporary overlay over the original trace with its extents defined by the trace area. When a multple different trace areas are used, trace parts begin to pile up on the original input trace file, called the base file. Figure 1 illustrates the concepts of the base file, trace areas and trace parts.
Figure 1. Trace Areas vs. Trace Parts
A trace with three trace areas defined. The user first modified the packets in area 1, followed by those in trace area 2 and area 3, and finally again the packets in area 1. Each modification caused the creation of new trace parts that got stacked up onto the base trace.
Making sure that the trace parts are properly maintained, overlayed and disposed of is the job of the trace part manager, abbreviated TPM. Each trace has one. TPMs organize the trace parts in such a way that the user always sees a consistent view of the modified trace. In code, a TPM is an instance of type LND_TPM. The TPM also takes care of properly sequencing the packets when a trace file that has developed multiple trace parts is saved back to disk (illustrated in Figure 2).
Trace parts can grow and shrink — after all, without this feature it would be fine to simply mmap() parts of the trace file. When a trace area is iterated and packets are written to temporary storage, the user is free to not write out all packets or introduce new ones. The TPMs keeps track of where the trace areas begin and end, and by what amount trace parts grow or shrink, relative to their original size.
Iterating over packets in libpcap is done using either a callback mechanism that allows only little control over the iteration, or using pcap_next() to get a single packet. libnetdude provides a higher abstraction for iterating packets. The packet iterator API allows you to use common for (...) { } loops and provides one function for each of the three for-loop steps (initialization, exit checking and counter adjustment). Packet iterators are instances of type LND_PacketIterator and are statically initialized. Avoiding allocating and releasing an iterator structure every time an iteration is performed is easier for the programmer, given that there is no automatic garbage collection in C.
There are multiple ways you can iterate over packets in a trace. The iteration mode is specified using one of the LND_PacketIteratorMode constants when the iterator is initialized:
LND_PACKET_IT_SEL_R assumes that you have loaded a number of packets into memory, starting at the current location in the trace (this is done using the libnd_tpm_load_packets() call). Within the loaded packets, you can select individual packets using libnd_tp_select_packet(). This iterator mode will only iterate over those packets that are currently in memory and have been selected, and assumes that no modifications to these packets are made.
LND_PACKET_IT_SEL_RW is similar to LND_PACKET_IT_SEL_R but allows modifications to the iterated packets.
LND_PACKET_IT_PART_R is used to request iteration of all packets currently loaded into memory and assumes that no modifications are made.
LND_PACKET_IT_PART_RW is similar to LND_PACKET_IT_PART_R, but allows modifications to the iterated packets.
LND_PACKET_IT_AREA_R is used to request iteration over packets in a trace area, without any modifications to the packets.
LND_PACKET_IT_AREA_RW is similar to LND_PACKET_IT_AREA_R, but allows modifications to the iterated packets. The difference between the read-only and the read-write mode is bigger here compared to the other options, because iteration of a trace area in read-write mode will lead to the creation of a new trace area, whereas the other mode will not.
#include <libnd.h> LND_Trace *trace; LND_PacketIterator pit; /* NOT a pointer */ LND_TraceArea area; libnd_init(); /* Open a tracefile: */ if (! (trace = libnd_trace_new("foo.trace"))) { /* Didn't work -- appropriate error handling. */ } /* Now set the trace area to the second half of the trace. * The default iteration mode of a trace is LND_PACKET_IT_AREA_R, * so we can either set it for the trace or just for the iterator. * Here we do the latter. */ libnd_trace_area_init_space(&area, 0.5, 1.0); libnd_trace_set_area(trace, &area); |
for (libnd_pit_init_mode(&pit, trace, LND_PACKT_IT_AREA_RW); libnd_pit_get(&pit); libnd_pit_next(&pit)) { /* Now just rely on the library and the protocol plugins to fix the packet * for us if it's currently broken: */ libnd_packet_fix(libnd_pit_get(&pit)); } |
![]() | Note that if you have other conditions in your loop that may lead to breaking out of the loop, you MUST call libnd_pit_cleanup() when doing so. The packet iterators normally do this for you when they hit the end of the iteration, but when you stop the iteration before that, you have to take care of that. |
Packet iteration is nice but only helpful when you can skip the packets you don't care about. libnetdude provides a filtering framework that allows stateful packet filtering where the actual filtering condition is entirely up to you.
Packet filters in libnetdude are instances of type LND_Filter. Their implementation depends on function pointers you provide for initialization, filtering, and cleanup. Every filter has a name, and possible statekeeping data that are used during the filtering process. A new filter is created using libnd_filter_new().
![]() | Keep in mind that if you do not require some of the callbacks (like when your filtering is not stateful), you do not need to define and pass all of the callbacks. |
![]() | A packet passes a filter when your filtering function returns TRUE for it. |
#include <libnd.h> LND_Trace *trace; LND_PacketIterator pit; /* NOT a pointer */ LND_TraceArea area; libnd_init(); /* Open a tracefile: */ if (! (trace = libnd_trace_new("foo.trace"))) { /* Didn't work -- appropriate error handling. */ } |
#define PACKET_SIZE_LIMIT 100 gboolean filter_length(LND_Filter *filter, LND_Packet *packet, void *filter_data) { return (packet->ph.len <= 100) ? TRUE : FALSE; } |
LND_Filter *filter; if (! (filter = libnd_filter_new("size filter", NULL, /* no initialization needed */ filter_length, NULL, /* no cleanup needed */ NULL, /* no state, so no state cleanup needed */ NULL))) /* no statekeeping needed */ { /* Didn't work -- appropriate error handling. */ } libnd_trace_add_filter(trace, filter); for (libnd_pit_init(&pit, trace); libnd_pit_get(&pit); libnd_pit_next(&pit)) printf("Packet with header length %i passed the filter\n", libnd_pit_get(&pit)->ph.len); |
Feature plugins are libnetdude's mechanism to provide reusable, useful packet-handling modules and can contain arbitrary code. Examples would be advanced filters ("drop all incomplete TCP flows"), address mappers, or statistical analyzers. For details on writing a feature plugin, see the chapter below.