packet-analyzer-dpi: How a C++ DPI Engine Turns One Flow Into One Thread
A deep dive into manual packet parsing, TLS SNI extraction, and a fast-path architecture that keeps stateful inspection local, parallel, and surprisingly readable.
- The engine’s core trick is not packet parsing, but keeping each flow on one worker so connection state never has to be shared globally.
- Manual byte-level parsing turns Ethernet, IP, TCP, UDP, and TLS into readable structures without hiding how the wire format actually works.
- TLS SNI gives the system just enough hostname visibility to classify encrypted traffic before the rest of the payload disappears behind encryption.
- The rule layer is where inspection becomes policy, with wildcard domains, port filters, and shared locks making the system usable under concurrency.
The smartest thing about packet-analyzer-dpi is not that it inspects packets. Plenty of projects can do that. The interesting move is more specific: it routes every packet in a conversation to the same thread, so the engine can keep state local while still moving fast.
That choice changes everything. Once a flow is affinity-bound, the code can classify traffic once, remember the result, and stop paying the cost of rediscovery on every packet. In a DPI engine, that is the difference between a demo and a usable fast path.
The real trick: keeping each connection on one thread
| Traditional centralized inspection | packet-analyzer-dpi fast path |
|---|---|
| Packets can bounce between workers, so state has to be shared or reconstructed. | The same flow is routed to the same thread, so state stays local. |
| Locking tends to grow with throughput. | Most packets avoid cross-thread coordination once the flow is classified. |
| Each packet may need repeated checks. | The engine can classify once and reuse the result. |
| Visibility is centralized, but contention is too. | Visibility is distributed with less friction. |
| The design is simpler to describe than to scale. | The design is built around scaling from the start. |
From raw bytes to readable headers
The repo treats packets as structured binary, not magic. `PacketParser` walks the buffer by offset, peels off Ethernet, IPv4, TCP, and UDP headers, then turns the result into a `ParsedPacket` the rest of the engine can reason about.
// Conceptual shape of the parser path
const uint8_t* cursor = frame.data();
EthernetHeader eth = readEthernet(cursor);
cursor += sizeof(EthernetHeader);
IPv4Header ip = readIPv4(cursor);
cursor += ip.headerLengthBytes();
if (ip.protocol == Protocol::TCP) {
TCPHeader tcp = readTCP(cursor);
cursor += tcp.headerLengthBytes();
} else if (ip.protocol == Protocol::UDP) {
UDPHeader udp = readUDP(cursor);
cursor += sizeof(UDPHeader);
}
ParsedPacket packet = buildParsedPacket(eth, ip, cursor);
That is why this codebase reads like a teaching tool. It does not hide the network stack behind a convenience layer. It shows you the stack, then asks you to build on it.
The encrypted leak: how SNI gives the engine a name
The sharpest tension in the repo lives in `SNIExtractor`. Even when the payload is encrypted, the TLS ClientHello still exposes the server name early enough for the engine to classify the flow. That makes hostname-based filtering possible before the door closes.
The point is not that encryption is broken. The point is narrower: modern traffic still leaks a little context at the boundary, and this engine is built to catch that context fast.
| What the engine can see | What stays hidden |
|---|---|
| TLS ClientHello metadata, including SNI. | Application payload after encryption takes over. |
| Protocol headers and connection patterns. | The actual content of most HTTPS sessions. |
| Enough to classify a destination. | Not enough to inspect the whole conversation. |
That narrow window is the whole game. DPI in this form is less about omniscience and more about timing.
Fast path first, slow path only when needed
`FastPathProcessor` is where the architecture cashes in on its routing decision. One worker owns the connection tracker for a flow, so it can remember whether the session has already been classified, whether a rule matched, and whether later packets can take the cheaper route.
The performance win is simple: do the expensive work once, then reuse the answer. That is a classic systems pattern, but here it is wired directly into packet routing.
| First packet in a flow | Later packets in the same flow |
|---|---|
| May trigger deeper inspection and classification. | Reuse the stored decision from the connection tracker. |
| Pays the cost of SNI parsing and rule checks. | Skips repeated parsing when possible. |
| Establishes the flow’s identity. | Benefits from that identity. |
Rules as the final gate
`RuleManager` turns inspection into policy. It handles blacklisted IPs, wildcard domains like `*.example.com`, and port filters, then exposes that policy through `std::shared_mutex` so many workers can read at once while updates stay controlled.
// Policy checks are read-heavy, so shared locking matters.
std::shared_lock lock(ruleMutex);
if (isBlockedIP(packet.dstIP)) {
return Action::BLOCK;
}
if (matchesWildcardDomain(packet.sni, domainRules)) {
return Action::BLOCK;
}
if (blockedPorts.contains(packet.dstPort)) {
return Action::BLOCK;
}
return Action::ALLOW;
| Policy layer traits | Why it matters |
|---|---|
| Wildcard domain support | Lets the engine block families of destinations, not just exact hosts. |
| Port filtering | Adds another simple control surface when hostname data is unavailable or incomplete. |
| Shared mutex reads | Makes rule checks cheap under concurrent load. |
| Centralized rule ownership | Keeps updates coherent while workers stay mostly read-only. |
This is where the repo becomes more than a parser. It is a filter engine with a clear boundary between observation and enforcement.
Why this repository feels educational, not just functional
The code is valuable because it leaves the seams visible. You can see the packet parser, the TLS extractor, the flow-affine worker model, and the rule layer without a framework flattening the details away.
That makes it useful in a way many production systems are not. It shows the moving parts of DPI at a scale you can still hold in your head: binary parsing, per-flow state, lock-minimized concurrency, and policy checks that do not fight the data path.
| Why it teaches well | Why it still feels practical |
|---|---|
| Minimal dependencies keep the code readable. | The architecture still targets real throughput concerns. |
| Manual parsing exposes protocol structure. | Consistent hashing keeps the state model efficient. |
| Modern C++ idioms reduce accidental complexity. | Shared locks and atomics support concurrent access cleanly. |
The bigger context is less comfortable. DPI is a cat-and-mouse game with privacy tech, and the window this project uses is getting narrower as protocols evolve. That tension is part of the story, not a footnote.





