Deep-Packet-Inspection: How This C++ Engine Turns Network Traffic into a Sharded State Machine

A close look at the flow-hashing trick, layered parsing pipeline, and SNI-based classification that let the engine inspect encrypted traffic without central locks.

8 to 10 min read • View on GitHub • More from Swaymbhu-git

A packet enters a machine with layered chambers for Ethernet, IP, and TCP or UDP, then reaches a hash gate that sends it into one of several worker lanes. In the final chamber, a magnifying lens pulls out a destination clue and hands it to a policy stamp. The image explains that inspection happens after the packet is routed to the right worker lane.
The engine’s real trick is not parsing alone. It is turning packet inspection into a routing problem first, then a classification problem.
Key Takeaways

The Packet’s Journey Starts Before Inspection

The surprising thing about Deep-Packet-Inspection is that it does not begin with a classifier. It begins with a routing decision. A packet is parsed layer by layer, then assigned to a worker lane by its FiveTuple so the rest of the system can treat that flow as owned state, not a shared problem.

That order matters. If you start with a global connection table, you get locks, contention, and a lot of thread coordination. This repo flips the sequence: first establish where the packet belongs, then inspect it.

This diagram shows the project’s mental model: packets are routed into ownership, then classified, then acted on. The cleverness is in how little shared coordination the hot path needs.

Why the FiveTuple Matters

The engine’s identity model is the FiveTuple: source IP, destination IP, source port, destination port, and protocol. That is the smallest useful fingerprint for a network flow, and it is enough to keep request and response together without confusing one conversation for another.

struct FiveTuple {
    uint32_t src_ip;
    uint32_t dst_ip;
    uint16_t src_port;
    uint16_t dst_port;
    uint8_t protocol;

    FiveTuple reverse() const {
        return FiveTuple{dst_ip, src_ip, dst_port, src_port, protocol};
    }
};

That tiny reverse() method carries a lot of weight. It is how the engine recognizes that a reply packet belongs to the same flow as the original request, even though the direction is inverted and the tuple fields are swapped.

A close-up mechanical key shaped like a FiveTuple turns inside a splitter. One path sends the flow through a single narrow gate into one worker lane, while another identical packet follows the same lane behind it. A wider unused gate labeled by implication sits in the background, showing the rejected alternative of shared contention. The image explains deterministic worker assignment for bidirectional flow tracking.
Same conversation, same worker. That is the architecture’s central promise, and it is what keeps state coherent under concurrency.

The Shared-Nothing Trick

This is the repo’s best architectural move. The load balancer does not just spread work around. It spreads work in a way that preserves flow affinity, so a packet conversation lands on one fast path processor and stays there.

ApproachWhat it seesConcurrency modelState handlingTrade-off
Naive L3 or L4 filterIP and port onlyEasy to parallelize, but blind to application contextAlmost no flow memoryFast, but shallow
Global-lock DPI engineDeep packet data plus a shared connection tableMany threads fighting over one hot structureCentralized and contendedCorrect, but expensive at scale
This repo’s flow-hashed pipelineLayered packet data plus destination clues like SNIPackets from one flow stay on one workerThread-local ownership with controlled shared policy readsMore scalable, with less coordination

That design removes the worst kind of synchronization from the hot path. The connection tracker still matters, but it stops being a shared choke point and becomes a worker-local memory of the flow.

How Encrypted Traffic Still Gives Up a Clue

The project does not try to break encryption. It looks for the information encryption leaves behind. TLS Client Hello messages can expose SNI, and HTTP or QUIC paths can still reveal enough host identity to make a policy decision.

Layer 3: Deep packet inspection Even over HTTPS, the initial TLS handshake leaks the destination domain in plaintext, the SNI field. DPI middleboxes read that and kill the connection before encryption kicks in. Hardest layer to bypass.

Akshansh Gusain, akshanshgusain · @akshanshgusain on X

That is the real asymmetry here. The engine does not need plaintext content to be useful. It needs enough metadata to classify the destination, then it can apply policy without stepping outside the encryption boundary.

The Connection Tracker Is the Memory of the Engine

The connection tracker is where packets become state. The flow lifecycle in the repo moves through familiar stages: NEW, ESTABLISHED, CLASSIFIED, and BLOCKED. That progression is more than bookkeeping. It is how the engine remembers what it has already decided.

std::unordered_map<FiveTuple, ConnectionState, FiveTupleHash> connections;
std::shared_mutex rules_mutex;

// The hot path updates flow state, while stale entries are cleaned up later.
cleanupStale(connections);

Rules Stay Readable Because Writers Rarely Show Up

The rule manager uses a read-heavy concurrency pattern, which is exactly what a DPI policy engine needs. Most packets ask the same question: is this destination allowed? Very few events rewrite the policy itself.

Design choiceWhy it fits DPIRisk if chosen badly
std::shared_mutex for rulesMany readers, rare writersWriter starvation if reads are extreme
Single mutex for everythingSimple to reason aboutTurns policy checks into a bottleneck
Per-packet rule reloadsAlways currentWasteful and unstable under load
std::shared_lock<std::shared_mutex> lock(rules_mutex);
auto rule = rule_manager.find(app_type);
if (rule && rule->blocked) {
    return Decision::BLOCK;
}

That is the project’s practical streak in one choice. It is not chasing theoretical purity. It is tuning the lock strategy to the actual workload.

Why QUIC Makes the Job Harder

QUIC raises the difficulty because it is UDP-based and encrypted early. That means the old TCP assumptions do not hold, and the engine has to lean harder on the few signals that remain visible at the start of a flow.

Protocol pathWhat the engine can inspectWhy it is hard
TCP plus TLSClient Hello, SNI, and flow behaviorLonger-lived handshake structure gives the parser more to work with
QUIC over UDPInitial handshake metadata and SNI-like cluesEncryption starts early and the transport is connectionless
Plain L3 or L4 filteringIP and port onlyToo shallow for service-level policy

That makes the QUIC path a useful stress test for the whole architecture. If the flow-hashed pipeline can still identify destinations here, the design is keeping up with modern traffic instead of pretending modern traffic does not exist.

What This Repo Is Really Building

This is not just a DPI library. It is a template for a policy-enforcing network appliance built in modern C++17, with PCAP-centric processing, modular parsing, and enough concurrency structure to grow into live capture support later.

The code suggests a system that wants to scale by composition, not by centralization. Each stage does one job, each flow belongs to one worker, and the policy layer stays readable because most of the write pressure never reaches it.