Plugin API
fpnt achieves its zero-LoC extensibility through a dynamic function-based plugin architecture. The framework dynamically loads shared object (.so) files containing custom C++ functions for preprocessing data and generating keys. This page explains how to write these plugins, utilize them in CSV schemas, and best practices.
Overview of the Plugin System
When fpnt is executed, it loads a shared library (usually libFPNT_PLUGINS.so). Inside this library, fpnt looks for specific C-linkage functions (extern "C") that match the required signatures. There are two primary types of plugin functions:
- Key Generators (
genKey_*): Define how packets are aggregated into higher-level granularities (e.g., flows, flowsets). - Preprocessing Functions (
P_*): Define how to extract, transform, or aggregate features for a specific record.
With the v0.40 update, the framework introduced an Index-Based API (P_*_idx) for preprocessing functions to bypass slow string lookups and instead use direct array indexing, significantly improving throughput.
Key Generation Functions
Key generators determine how records are grouped. Every packet parsed from tshark is passed to the key generators for the configured granularity levels (e.g., flow, flowset).
Function Signature
extern "C" void genKey_flow_default( fpnt::PluginContext& context, const std::vector<std::string>& parent_values, std::string& generated_key, int64_t pkt_idx );
context: Provides access to theDispatcherand current state.parent_values: If this granularity level depends on a parent level, its values are passed here.generated_key: The output parameter. You must assign the resulting key string to this variable. If you assign an empty string, the packet is ignored for this granularity level.pkt_idx: The direct array index of the current packet record in theDispatcher's memory.
Example: A Simplified 5-tuple Flow Key
To group packets by a standard 5-tuple (Source IP, Destination IP, Source Port, Destination Port, Protocol):
extern "C" void genKey_flow_5tuple(fpnt::PluginContext& ctx, const std::vector<std::string>& parent, std::string& key, int64_t pkt_idx) { nlohmann::json& pkt = ctx.dispatcher->getRecordByIdx(0, pkt_idx); // 0 is packet level std::string sip = pkt["ip.src"].is_string() ? pkt["ip.src"].get<std::string>() : ""; std::string dip = pkt["ip.dst"].is_string() ? pkt["ip.dst"].get<std::string>() : ""; std::string sport = pkt["tcp.srcport"].is_string() ? pkt["tcp.srcport"].get<std::string>() : ""; std::string dport = pkt["tcp.dstport"].is_string() ? pkt["tcp.dstport"].get<std::string>() : ""; std::string proto = pkt["ip.proto"].is_string() ? pkt["ip.proto"].get<std::string>() : ""; // Create a bidirectional key if (sip > dip) { std::swap(sip, dip); std::swap(sport, dport); } key = sip + "," + dip + "," + sport + "," + dport + "," + proto; }
Preprocessing Functions
Preprocessing functions mutate or aggregate features. They are executed in the order defined in the CSV schemas.
Index-Based API (
extern "C" void P_math_add_idx(fpnt::PluginContext& ctx); Although the signature takes only the PluginContext, the context contains the pre-resolved indices:
ctx.current_idx: The index of the record currently being processed.ctx.target_idx: The field index where the result should be stored.ctx.args_idx: Astd::vector<int>containing the resolved indices of the arguments provided in the CSV.ctx.getChildIdxs(): Returns the indices of all child records (e.g., if processing a flow, it returns the indices of all packets belonging to this flow).
Example: Child Sum
extern "C" void P_childsum_d_idx(fpnt::PluginContext& ctx) { double result = 0; int target_field_idx = ctx.args_idx[0]; // The field index in the child records for (size_t child_idx : ctx.getChildIdxs()) { nlohmann::json& cnt = ctx.getChildRecordByIdx(child_idx); // Assuming the child record's field is stored as a string or number if (!cnt[target_field_idx].is_null()) { std::string val_str = cnt[target_field_idx].get<std::string>(); result += std::stod(val_str); } } // Store the aggregated result ctx.dispatcher->setRecordByIdx(ctx.current_idx, ctx.target_idx, std::to_string(result)); }
Commonly Used P_*() Functions
The framework comes with several built-in mathematical and utility plugins.
- Aggregation (Child to Parent)
P_childsum_ll_idx/P_childsum_d_idx: Sums integer/double values from all child records.P_childmean_idx: Calculates the mean of child values.P_childstdev_idx: Calculates the sample standard deviation of child values.P_childmax_d_idx/P_childmin_d_idx: Finds the maximum/minimum double value among children.
- Math Operations
P_math_add_idx,P_math_sub_idx,P_math_mul_idx,P_math_div_idx: Performs basic arithmetic on fields within the current record.
- Sequence Generation
P_seq_idx: Collects a specific field from all child records and formats it as a comma-separated sequence string (e.g., for SPLT extraction).
Specifying Plugins in CSV
In your schema CSV files (e.g., output_pkt.csv, output_flow.csv), you define which functions to use and their corresponding options. The functions and their options are written in separate columns.
Syntax Rules:**
- Multiple Functions: You can apply multiple preprocessing functions sequentially to a single output field. Functions are separated by semicolons (
;) in thepreprocessing_functionscolumn. - Options/Arguments: The corresponding arguments for each function are written in the
options(orarguments) column, also separated by semicolons (;) in the exact same order. Single Argument Limit: Currently, each preprocessing function accepts only one argument.
Example
output_pkt.csvsnippet:**
"protocol","Protocol","P_cpy;P_fill1","ip.proto;ipv6.nxt"
In this example:
- The first function
P_cpyis executed with the argumentip.proto. - The second function
P_fill1is then executed with the argumentipv6.nxt. This sequential execution allows for building a per-field preprocessing pipeline directly within the CSV schema.
Precautions and Best Practices
- Handling Missing Data: Not all packets contain all fields (e.g., a UDP packet lacks TCP flags). Always check if a field exists or
is_null()before accessing it in a plugin to prevent crashes. - Type Casting (
std::stod,std::stoll): When converting strings to numbers, ensure the string is not empty. C++std::stodthrows anstd::invalid_argumentexception on empty strings, which will crash thefpntprocess. Use wrapper functions or check!str.empty()beforehand. - Stateless Plugins:
fpntuses file-level multiprocessing viafork(). This means global variables are memory-isolated between different PCAP files. However, within a single PCAP processing pipeline, plugins must remain stateless or cleanly reset their state, as they are invoked repeatedly for every record. - Memory Leaks: When writing custom C++ plugins, avoid allocating raw memory (
new,malloc) without freeing it. Use standard C++ containers (std::vector,std::string) or smart pointers. - Index Validity: When using the
_idxAPI, rely exclusively onctx.args_idxandctx.target_idx. Do not hardcode indices, as the field indices are dynamically assigned by theDispatcherbased on the CSV schema at runtime.