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.

9 min read • View on GitHub • More from Shivaay100

A wide editorial scene shows a network artery splitting into several worker lanes. Raw packet fragments enter as a bundled stream, pass through a gate keyed by the 5-tuple, and then stay attached to a single lane. A small TLS ClientHello opens just enough to reveal a hostname tag before the traffic becomes opaque again, explaining how flow affinity and selective visibility work together.
One flow, one worker. The architecture keeps connection state local while still extracting just enough meaning from encrypted traffic to classify it fast.
Key Takeaways

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

Consistent hashing makes stateful inspection practical. The same 5-tuple always lands on the same worker, so the tracker can stay local and cheap.

Traditional centralized inspectionpacket-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);
A close-up cutaway shows layered protocol headers being peeled back like mechanical shells. Ethernet sits on top, then IPv4, then TCP or UDP, and finally a TLS ClientHello compartment where a small SNI label becomes visible before the rest seals shut. The image explains how manual parsing turns raw bytes into a staged inspection pipeline.
The code reads packets like layered machinery. Each header reveals just enough to decide what to inspect next.

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 seeWhat 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 flowLater 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 traitsWhy it matters
Wildcard domain supportLets the engine block families of destinations, not just exact hosts.
Port filteringAdds another simple control surface when hostname data is unavailable or incomplete.
Shared mutex readsMakes rule checks cheap under concurrent load.
Centralized rule ownershipKeeps 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 wellWhy 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.