How to create custom Codec plugins for Snort 3: the Codec class, decode(), get_protocol_ids(), protocol ID ranges, the CodecApi structure, and a complete working example.
Codecs are Snort’s layer-by-layer packet decoders. Every protocol Snort understands — Ethernet, IP, TCP, UDP, and more — is implemented as a Codec. Because codecs are fully pluggable, you can add support for new or proprietary protocols without touching the Snort core.
When a packet arrives, Snort’s PacketManager calls codecs in sequence. Each codec receives the raw bytes that start at the current layer and is responsible for:
Validating that enough bytes are present for the protocol header.
Setting CodecData::lyr_len to the number of bytes consumed by this layer.
Setting CodecData::next_prot_id to identify the next protocol so the correct codec can be chained in.
If a codec returns false, Snort discards the packet.
Called when Snort must build a response packet because a rule with react, reject, or rewrite fired. Encoding starts from the innermost layer and works outward. You must call Buffer::allocate() before writing to the output buffer.
format() — packet rebuild
Called during TCP stream reassembly and IP defragmentation. Typically swaps source and destination fields, or does nothing.
update() — length recalculation
Called during reassembly to update length and checksum fields. Unlike format(), it only updates lengths, not addresses.
log() — packet logging
Called when the log_codecs logger (or any custom logger that calls PacketManager::log_protocols) is active.
If encode(), format(), or update() are not implemented, Snort will not error — but active response and stream reassembly for your protocol may not function correctly.
bool ExCodec::decode(const RawData& raw, CodecData& codec, DecodeData&){ // Reject the packet if there aren't enough bytes for the header. if ( raw.len < Example::size() ) return false; const Example* const ex = reinterpret_cast<const Example*>(raw.data); // Tell Snort how long this layer is ... codec.lyr_len = Example::size(); // ... and which protocol comes next. codec.next_prot_id = static_cast<ProtocolId>(ntohs(ex->ethertype)); return true;}
How chaining works: if the 32-byte packet below arrives:
static Codec* ctor(Module*){ return new ExCodec; }static void dtor(Codec* cd){ delete cd; }static const CodecApi ex_api ={ { PT_CODEC, sizeof(CodecApi), CDAPI_VERSION, 0, // plugin version 0, // features nullptr, // options EX_NAME, EX_HELP, nullptr, // mod_ctor (no Module needed) nullptr, // mod_dtor }, nullptr, // pinit — called at Snort startup nullptr, // pterm — called at Snort exit nullptr, // tinit — called at packet-thread startup nullptr, // tterm — called at packet-thread exit ctor, // codec constructor dtor, // codec destructor};SO_PUBLIC const BaseApi* snort_plugins[] ={ &ex_api.base, nullptr};
EX_NAME in the CodecApi must exactly match the string passed to Codec(EX_NAME) in the constructor. A mismatch will prevent Snort from loading the codec.
For codecs that sit at the root of the decoding chain (i.e. they receive the packet directly from libpcap), register by DLT rather than by protocol ID:
// framework/codec.hstruct CodecApi{ BaseApi base; // common plugin header — must be first CdAuxFunc pinit; // initialize global plugin data (may be nullptr) CdAuxFunc pterm; // clean up pinit() (may be nullptr) CdAuxFunc tinit; // initialize thread-local data (may be nullptr) CdAuxFunc tterm; // clean up tinit() (may be nullptr) CdNewFunc ctor; // construct Codec — required CdDelFunc dtor; // destroy Codec — required};