Snort++ Multi-thread Overview

Snort2 was not multi-threaded but Snort++ is. Snort++ can start threads that process either network interfaces or PCAP files. So if you're interested in the traffic from two network interfaces (e.g., eth0 and eth1), Snort++ can create two packet-processing threads and have each thread inspect the packets (technically, the link-layer frames) from one of these interfaces
I question how high the demand for this is.
.
The code that creates the packet-processing threads is fairly complex but the concepts are fairly easy to understand.
Every process starts with a single thread (represented by the "Main Thread" in the image above). The Main Thread initializes Snort++ (using the settings in the Snort++ configuration file - typically snort.lua), creates the packet-processing threads (the "Packet Threads"), and later controls these threads (e.g., requests that the Packet Threads start and stop processing packets). Each Packet Thread is assigned an interface (or PCAP file) during the thread's DAQ initialization. Once DAQ is initialized for the Packet Thread, the Main Thread can request that the Packet Thread start processing packets. DAQ will then send packets that it sees on a given network interface (or in a PCAP file) to the appropriate Packet Thread. The Main Thread accomplishes this by invoking a callback function (typically packet_callback()) within the Packet Thread with the packet as an argument to the function. This callback function implements the packet-processing pipe.
name:

website:

comment:


Pigs

Pigs help Snort++ do its multithreading work. Snort++'s Pig objects specifically do two things:
1) Create and destroy the Packet Threads.
2) Send requests to the Packet Threads for the Packet Thread to initialize itself and for the Packet Thread to begin processing packets .
After Snort++'s initialization is complete, a Pig object will have been created for each packet-processing thread and the main loop of the Main Thread is entered. The Main Thread will then initialize the Packet Threads and prompt them to start processing packets.
There will ideally be one packet thread for each interface (or PCAP file) specified
The number of packet threads can be specified by the --max-packet-threads setting or this value can be determined by Snort++ automatically. Threads make the most sense on multiprocessor or multicore systems but it is not necessary to have a multiprocessor or multicore system for an application/service (daemon) to have multiple threads.
and that packet thread will go on its own processor (or core).
The main loop, which is executed in the Main Thread, creates the Packet Threads. It's important to understand that the main loop does not process packets. It simply creates the Packet Threads and then initiates (and later stops or pauses) the packet processing in these Packet Threads.
Each Packet Thread has an associated Pig object and each Pig object has an associated Analyzer object. This Analyzer object fulfills two roles: it supplies the function (operator()) that is executed by the new thread upon creation and enables communication between the Packet Threads and the Main Threads. We will discuss both of these roles in the coming sections.
name:

website:

comment:


Analyzer States

The Analyzer objects follow a state machine.
Most of the transitions occur after the Main Thread puts an AnalyzerCommand-derived (e.g., ACStart) object in a Packet Thread's queue. We'll talk about that a little later. Here's what happens for each state transition:
NEW -> INITIALIZED: The new Packet Thread is created by the Main Thread and the network interface (e.g., eth0) for the Packet Thread is set. Packets from this interface will ultimately be handed off by DAQ to the Packet Thread (specifically, to its packet_callback() function) responsible for the interface
The DAQ instance is also tested by the Packet Thread to determine if the DAQ-related parameters will work. For example, if the specified network interface doesn't exist, DAQ will report an error.
.
INITIALIZED -> STARTED: This is a relatively unimportant transition. If there are Berkeley Packet Filters (BPF) specified, they are applied during this transition.
STARTED -> RUNNING: The Packet Thread calls the thread-specific initialization routines from several modules and finally calls the important function SFDAQInstance::acquire(), which tells DAQ to send packets from the network interface specified in the steps above to a specific function (main_func - which is set to packet_callback() if Snort++ is configured as an IDS). After SFDAQInstance::acquire() returns, packets from the specified network interface will be sent to main_func
There are two state transitions that are not particularly interesting. Using the Snort++ shell, a Packet Thread can be paused (the pause command) and then resumed (the resume command).
RUNNING -> PAUSED: When the pause Snort++ shell command is executed, the Analyzer object is simply moved out of the RUNNING state and put in the PAUSED state. When it's in the PAUSED state, Snort++ will not process any packets for the given thread/interface until the resume Snort++shell command is executed.
PAUSED -> RUNNING: The opposite of the RUNNING -> PAUSED transition. Snort++ moves the Analyzer object to the RUNNING state and the packet thread continues to process packets.
.
name:

website:

comment:


Packet Thread Creation and Initialization, Step by Step

To keep things simple, we will study an example with only a single Packet Thread, which we will call "Packet Thread 0".
When the Snort++ process starts, only the Main Thread is created. The Main Thread creates the pigs[] array.
After the creation of the Pigs and the containing pigs[] array, the main loop is executed. As noted above, the main loop (which is executed by the Main Thread) does not process any packets but instead creates and destroys the Packet Threads as well as help the Packet Threads move to the different states. Until the packet threads are all RUNNING, the main loop simply moves each Pig to the next state. For example, if there are two Pigs (each corresponding to a different thread), the main loop will move pig[0] to state INITIALIZED and then it will move pig[1] to INITIALIZED and then it will move pig[0] to STARTED and then pig[1] to STARTED and so on.
Each element of the pig[] array corresponds with a Packet Thread and an associated interface (e.g., eth0) or PCAP file. Before the Main Thread can create Packet Thread 0, the associated Pig must create an Analyzer object, which enables the Main Thread to communicate with the (soon to be created) Packet Thread 0. The Analyzer class also supplies Packet Thread 0 with the function that the Packet Thread executes upon creation (Analyzer::operator()).
Once the Analyzer object has been created (its state is initialized to NEW), Packet Thread 0 can be created
Creating a new thread is fairly tricky.
.
The function that the new Packet Thread 0 executes upon creation is Analyzer::operator().
name:

website:

comment:


Analyzer::operator()

Note that both the Main Thread and Packet Thread 0 have read/write access to state and the queues (pending_work_queue and completed_work_queue)
Main Thread and Packet Thread 0 share the Analyzer object because a reference (as opposed to a copy of) to the Analyzer object is passed in as an argument to the std::thread() function.
. Main Thread and Packet Thread 0 use the read/write access of state and the queues in their shared Analyzer object to communicate with each other. The Main Thread (using the Pig that is associated with Packet Thread 0) will add an AnalyzerCommand-derived object to the queue and Packet Thread will act accordingly. (I'll discuss the queue in a moment.)
However, before this communication between Main Thread and Packet Thread 0 begins, operator() does a few important initializations. The most important task that operator() performs is to initialize the Packet Thread 0's DAQ "instance." This initialization, among other things, tells DAQ which network interface (e.g., eth0) is the responsibility of Packet Thread 0. After initialization, all packets received on this interface will be handed off to Packet Thread 0
Snort++'s global settings are stored in the all-important global snort_conf variable (which reflects the Snort++ configuration file(s) - typically snort.lua) and the host settings (e.g., 192.168.1.100 is a Windows system) are stored in the curr_cfg variable.
There is one Swapper object per Pig. Swapper objects allow a smooth transition in a Packet Thread from old global and host settings to new global and host settings in the event that Snort++ is asked to reread its configuration or host files (typically due to a change in a configuration file). A Packet Thread must stop processing packets before it can "swap" global and host settings.

. operator() creates the SFDAQInstance object, which holds the DAQ settings for the thread. This includes the Packet Thread's associated network interface (e.g., eth0) and its callback function (typically packet_callback(), which we'll discuss a little later). DAQ also verifies that the settings (especially the interface) are valid
SFDAQInstance's member daq_hand is also initialized later. After DAQ is initialized, daq_hand reflects the interface (e.g., eth0) as well as the desired mode (e.g., passive). daq_hand is used in subsequent calls to the DAQ library to indicate the originator of the call.
The SFDAQInstance class is, of course, object-oriented. The DAQ library, on the other hand, is not. I'm guessing that the DAQ library will be converted from C to C++ at some point since the interaction between the SFDAQInstance methods and the DAQ library strike me as a little clumsy.
.
After SFDAQInstance's fields are initialized, the shared Analyzer object is moved to state INITIALIZED.
name:

website:

comment:


analyze()

At this point, Packet Thread's analyze() is called. analyze() waits for the Main Thread to put AnalyzerCommand-derived objects in the queue. After the Main Thread sees that the state has been moved to INITIALIZED, it will put an ACStart object in the queue.
handle_command(), called by analyze(), handles the objects in the pending_work_queue queue and moves the Analyzer object to the different states.
The most important method of the AnalyzerCommand-derived classes is execute(). execute() typically does some task and then moves Analyzer::state to the next state. At this point, handle_command() calls the ACStart::execute() method to set DAQ's BPF filter (as described above) and move the state to STARTED. The ACStart object is then moved to completed_work_queue.
From here until all of the Packet Threads are processing packets, the Main Thread's handle() interacts with the Packet Thread's handle_command() to get the packet processing threads ready to start processing packets (for the most part, this means initializing DAQ). As long as the state isn't RUNNING, handle_command() does not call SFDAQInstance::acquire(). Once the Analyzer finally moves to the RUNNING state, it simply goes into a tight loop, each time calling SFDAQInstance::acquire(). We'll talk about SFDAQInstance::acquire() when Analyzer goes to the RUNNING state.
It's important to understand that Main Thread and Packet Thread 0 march in lock step. After the early initialization is done and the Analyzer object moves to the INITIALIZED state, Main Thread (specifically, Pig::queue_command()) will put AnalyzerCommand-derived objects in the queue and Packet Thread 0 will react to them.
Main Thread then puts an ACRun object in the queue.
When Packet Thread 0 runs again, it will call handle_command()
which will initialize several modules (by calling ACRun::execute) before setting state to RUNNING.
At last, SFDAQInstance::acquire() can be called.
At this point, DAQ has been set up and all of the necessary thread-specific module initializations have been done. SFDAQInstance::acquire() simply tells DAQ which function it should call to process a packet, which will typically be Snort::packet_callback(),
which implements the packet-processing pipeline
It's important to understand that SFDAQInstance::acquire() does not return after every packet. It typically returns after a certain time interval during which no packets are seen on the thread's associated interface.
.
name:

website:

comment: