Packet Analysis

The Packet Analysis plugin architecture handles parsing of packet headers at layers below Zeek’s existing Session analysis. In particular, this allows to add new link and network layer protocols to Zeek. This document provides an overview of the underlying architecture as well as an example-based walk-through. For further details, consider to take a look at the built-in packet analyzers as well as the packet analyzer tests.

The Flow of Packets

The basic packet flow through Zeek is as follows. First, an IOSource deals with getting the packets into Zeek. While an IOSource can be used to interface all sorts of capturing mechanisms, the default source makes use of libpcap to either read PCAP files or sniff an interface. Once acquired, a packet is handed into the packet analysis and processed layer by layer.

Nesting of Protocol Data Units (PDUs)

Nesting of Protocol Data Units (PDUs).

At the lower layers, Protocol Data Units (PDUs) typically consist of a header and a payload, where the payload is the next layer’s PDU and the header carries a numeric identifier that determines the encapsulated protocol (see figure above, where “ID” denotes the location of such a numeric protocol identifier within the header).

Each packet analyzer parses the packet’s header according to the implemented protocol, determines a suitable analyzer for the encapsulated protocol and hands its payload to that next analyzer. Once the IP layer is reached, packet analysis is finished and Zeek continues by constructing a session for the observed connection. After session analysis, which includes processing of TCP and UDP, the packet continues its journey into the land of application layer analyzers. There, Dynamic Protocol Detection is used to determine the application layer protocol and continue the analysis.

Packet Analyzer Configuration

The following script shows an example configuration of the Ethernet packet analyzer:

 1module PacketAnalyzer::ETHERNET;
 3export {
 4    ## Default analyzer
 5    const default_analyzer: PacketAnalyzer::Tag = PacketAnalyzer::ANALYZER_IP &redef;
 7    ## IEEE 802.2 SNAP analyzer
 8    global snap_analyzer: PacketAnalyzer::Tag &redef;
 9    ## Novell raw IEEE 802.3 analyzer
10    global novell_raw_analyzer: PacketAnalyzer::Tag &redef;
11    ## IEEE 802.2 LLC analyzer
12    global llc_analyzer: PacketAnalyzer::Tag &redef;
15event zeek_init() &priority=20
16    {
17    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x8847, PacketAnalyzer::ANALYZER_MPLS);
18    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x0800, PacketAnalyzer::ANALYZER_IP);
19    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x86DD, PacketAnalyzer::ANALYZER_IP);
20    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x0806, PacketAnalyzer::ANALYZER_ARP);
21    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x8035, PacketAnalyzer::ANALYZER_ARP);
22    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x8100, PacketAnalyzer::ANALYZER_VLAN);
23    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x88A8, PacketAnalyzer::ANALYZER_VLAN);
24    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x9100, PacketAnalyzer::ANALYZER_VLAN);
25    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x8864, PacketAnalyzer::ANALYZER_PPPOE);
26    }

Within zeek_init, various EtherType-to-PacketAnalyzer mappings are registered by using PacketAnalyzer::register_packet_analyzer. For example, for EtherType 0x8864, the packet’s payload is passed to the PPPoE analyzer.

The default_analyzer analyzer specifies which packet analyzer to use if none of the mappings matched. In case of Ethernet, we try to fall back to IP.

Furthermore, Ethernet needs to handle different types of frames, with three of them identified using the first payload bytes (see Wikipedia). As the EtherType needs to be interpreted with respect to the frame type in these cases, the Ethernet analyzer provides three additional configuration parameters, snap_analyzer, novell_raw_analyzer, and llc_analyzer. to configure analyzers that handle the different frame types.


There are a few conventions involved here:

  • The name of the module is expected to be PacketAnalyzer::<analyzer's canonical name>.

  • The default analyzer is expected to be named default_analyzer.

Packet analysis starts at a root analyzer that dispatches based on the link types obtained from the IOSource. Accordingly base/packet-protocols/root/main.zeek contains the following call to integrate the Ethernet analyzer:

PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ROOT, DLT_EN10MB, PacketAnalyzer::ANALYZER_ETHERNET);

Packet Analyzer API

Just like for other parts of Zeek, a plugin may provide a packet analyzer by adding a packet analysis component that instantiates an analyzer. The packet analyzer itself is implemented by inheriting from zeek::packet_analysis::Analyzer and overriding the AnalyzePacket() method. The following is an excerpt from a test case that shows the exemplary analysis of LLC:
 1bool LLCDemo::AnalyzePacket(size_t len, const uint8_t* data, Packet* packet)
 2    {
 3    // Rudimentary parsing of 802.2 LLC
 4    if ( 17 >= len )
 5        {
 6        packet->Weird("truncated_llc_header");
 7        return false;
 8        }
10    if ( ! llc_demo_message )
11        return true;
13    auto dsap = data[14];
14    auto ssap = data[15];
15    auto control = data[16];
17    event_mgr.Enqueue(llc_demo_message,
18        val_mgr->Count(dsap),
19        val_mgr->Count(ssap),
20        val_mgr->Count(control));
22    return true;
23    }

First, we verify that the size of the packet matches what we expect. If that is not the case, we create a weird using the Packet object that is passed along the chain of analyzers. To signal that the analysis failed, the method returns false. For valid packets, we just read some protocol-specific values. As of now, there is no mechanism to pass extracted meta data on to other analyzers. While it is possible to trigger events that receive these values as parameters, keep in mind that handling events for every packet can be extremely expensive. However, for our test case we defined an event as follows in a separate .bif file:

event llc_demo_message%(dsap: count, ssap: count, control: count%);

Before we can expect the event to be generated, we need to integrate the analyzer. The configuration might be included in the scripts that are shipped with the packet analyzer. For example, one could add a new EtherType by adding a call to PacketAnalyzer::register_packet_analyzer from within a zeek_init event handler. For the LLC example we redefine one of the additional constants:

redef PacketAnalyzer::ETHERNET::llc_analyzer = PacketAnalyzer::ANALYZER_LLC_DEMO;

In this example, packet analysis as well as all further analysis ends with the LLC analyzer. The ForwardPacket() method can be used to pass data to another packet analyzer. The method takes a pointer to the beginning of the data to process (usually the start of the payload in the current context), the length of the data to process, a pointer to the Packet object and an identifier. The identifier would be used to lookup the next analyzer based on which other analyzers were previously associated with LLC as a parent analyzer in a call to PacketAnalyzer::register_packet_analyzer. If there is no previously-registered analyzer that matches the identifier, it will fall back to the default_analyzer if available.

In case a packet analyzer requires initialization, e.g., reading additional configuration values from script-land, this can be implemented by overriding the Initialize() method. When overriding this method, always make sure to call the base-class version to ensure proper initialization.

With the addition of the transport-layer analyzer to the packet analysis framework, it’s now possible to register for ports as the identifier. This is natural, given that a port number is just another numeric identifier for moving from one protocol to another. Packet analyzers should call PacketAnalyzer::register_for_port or PacketAnalyzer::register_for_ports to ensure that the ports are also stored in the global Analyzer::ports table for use with BPF filters.

The packet analysis framework also provides a register_protocol_detection method that is used to register a packet analyzer to use protocol detection instead of using a numeric identifier. Analyzers can use this method and then override Analyzer::DetectProtocol to search the packet data for byte strings or other markers to detect whether a protocol exists in the data. This is similar to how DPD works for non-packet analyzers, but is not limited to pattern matching.


When writing your own packet analyzer, take a look into the existing code to identify idiomatic ways to handle tasks like looking up configuration values.