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.
- The repo’s core idea is to assign packet ownership before it tries to inspect payloads, which turns DPI into a concurrency problem it can actually manage.
- FiveTuple hashing keeps both directions of a conversation on the same worker, so state stays coherent without a global lock on the hot path.
- Encrypted traffic is still useful here because SNI and host metadata leak enough identity for policy decisions without breaking encryption.
- The design is pragmatic C++17 systems engineering: layered parsing, thread-local flow handling, shared rule reads, and controlled state cleanup.
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.
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.
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.
| Approach | What it sees | Concurrency model | State handling | Trade-off |
|---|---|---|---|---|
| Naive L3 or L4 filter | IP and port only | Easy to parallelize, but blind to application context | Almost no flow memory | Fast, but shallow |
| Global-lock DPI engine | Deep packet data plus a shared connection table | Many threads fighting over one hot structure | Centralized and contended | Correct, but expensive at scale |
| This repo’s flow-hashed pipeline | Layered packet data plus destination clues like SNI | Packets from one flow stay on one worker | Thread-local ownership with controlled shared policy reads | More 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.
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 choice | Why it fits DPI | Risk if chosen badly |
|---|---|---|
| std::shared_mutex for rules | Many readers, rare writers | Writer starvation if reads are extreme |
| Single mutex for everything | Simple to reason about | Turns policy checks into a bottleneck |
| Per-packet rule reloads | Always current | Wasteful 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 path | What the engine can inspect | Why it is hard |
|---|---|---|
| TCP plus TLS | Client Hello, SNI, and flow behavior | Longer-lived handshake structure gives the parser more to work with |
| QUIC over UDP | Initial handshake metadata and SNI-like clues | Encryption starts early and the transport is connectionless |
| Plain L3 or L4 filtering | IP and port only | Too 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.