This document explains everything about this project, from basic networking concepts to the complete Python code architecture. After reading this, you should understand exactly how packets flow through the system without needing to read the code.
- What is DPI?
- Networking Background
- Project Overview
- File Structure
- The Journey of a Packet
- Deep Dive: Each Component
- How TLS SNI and HTTP Host Extraction Works
- How Blocking Works
- Running
- Understanding the Output
- Extending the Project
Deep Packet Inspection (DPI) is a technology used to examine the contents of network packets as they pass through a checkpoint. Unlike simple firewalls that only look at packet headers such as source and destination IP, DPI looks inside the packet payload.
- ISPs: Throttle or block certain applications such as BitTorrent
- Enterprises: Block social media on office networks
- Parental controls: Block inappropriate websites
- Security: Detect malware or intrusion attempts
User Traffic (PCAP) -> [Python DPI Engine] -> Filtered Traffic (PCAP)
|
+-- Identifies apps (YouTube, Facebook, etc.)
+-- Blocks based on rules
+-- Generates reports
When you visit a website, data travels through multiple layers:
┌─────────────────────────────────────────────────────────┐
│ Layer 7: Application │ HTTP, TLS, DNS │
├─────────────────────────────────────────────────────────┤
│ Layer 4: Transport │ TCP (reliable), UDP (fast) │
├─────────────────────────────────────────────────────────┤
│ Layer 3: Network │ IP addresses (routing) │
├─────────────────────────────────────────────────────────┤
│ Layer 2: Data Link │ MAC addresses (local network)│
└─────────────────────────────────────────────────────────┘
Every network packet is like a Russian nesting doll, with headers wrapped inside headers:
┌──────────────────────────────────────────────────────────────────┐
│ Ethernet Header (14 bytes) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ IPv4 Header (20 bytes) │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ TCP Header (20 bytes) │ │ │
│ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Payload (Application Data) │ │ │ │
│ │ │ │ e.g., TLS Client Hello with SNI │ │ │ │
│ │ │ └──────────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
A connection, or flow, is uniquely identified by 5 values:
| Field | Example | Purpose |
|---|---|---|
| Source IP | 192.168.1.100 | Who is sending |
| Destination IP | 172.217.14.206 | Where it's going |
| Source Port | 54321 | Sender's application identifier |
| Destination Port | 443 | Service being accessed (443 = HTTPS) |
| Protocol | TCP (6) | TCP or UDP |
Why this matters:
- All packets with the same 5-tuple belong to the same connection
- If we block one packet of a connection, we should block all of them
- This is how we track conversations between computers
Server Name Indication (SNI) is part of the TLS/HTTPS handshake. When you visit https://www.youtube.com:
- Your browser sends a Client Hello message
- This message includes the domain name in plaintext before encryption starts
- The server uses this to know which certificate to send
TLS Client Hello:
├── Version: TLS 1.2
├── Random: [32 bytes]
├── Cipher Suites: [list]
└── Extensions:
└── SNI Extension:
└── Server Name: "www.youtube.com" ← We extract this
This is the key to DPI: even though HTTPS is encrypted, the domain name is visible in the first packet.
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Wireshark │ │ Python DPI │ │ Output │
│ Capture │ ──► │ Engine │ ──► │ PCAP │
│ (input.pcap)│ │ - Parse │ │ (filtered) │
└─────────────┘ │ - Classify │ └─────────────┘
│ - Block │
│ - Report │
└─────────────────┘
| Version | File | Use Case |
|---|---|---|
| Packet viewer | analyze_pcap.py | Learning, debugging, inspecting packets |
| DPI filter | dpi_engine.py | Blocking and writing filtered PCAPs |
packet_analyzer/
├── pcap.py # PCAP reader and writer
├── parser.py # Ethernet / IPv4 / TCP / UDP parsing
├── extractors.py # TLS SNI, HTTP Host, DNS name extraction
├── rules.py # Blocking rules and rule file loading
├── types.py # AppType, FiveTuple, Connection, stats objects
└── dpi_engine.py # Main DPI engine logic
analyze_pcap.py # Packet-by-packet inspection CLI
dpi_engine.py # DPI filtering CLI entry point
generate_test_pcap.py # Creates test data
test_dpi.pcap # Sample capture with various traffic
filtered.pcap # Example output capture
README.md # This file!
Let's trace a single packet through the Python engine.
from packet_analyzer.pcap import PcapReader
with PcapReader("capture.pcap") as reader:
for raw in reader.packets():
...What happens:
- Open the file in binary mode
- Read the 24-byte global header (magic number, version, etc.)
- Verify it's a valid PCAP file
for raw in reader.packets():
# raw.data contains the packet bytes
# raw.header contains timestamp and length
...What happens:
- Read a 16-byte packet header
- Read N bytes of packet data (N = header.incl_len)
- Stop when no more packets remain
from packet_analyzer.parser import PacketParser
parsed = PacketParser.parse(raw)What happens in packet_analyzer/parser.py:
raw.data bytes:
[0-13] Ethernet Header
[14-33] IPv4 Header
[34-53] TCP Header
[54+] Payload
After parsing:
parsed.src_mac = "00:11:22:33:44:55"
parsed.dest_mac = "aa:bb:cc:dd:ee:ff"
parsed.src_ip = "192.168.1.100"
parsed.dest_ip = "172.217.14.206"
parsed.src_port = 54321
parsed.dest_port = 443
parsed.protocol = 6 (TCP)
parsed.has_tcp = True
Parsing details:
- Ethernet header: destination MAC, source MAC, EtherType
- IPv4 header: version, header length, TTL, protocol, source IP, destination IP
- TCP header: source port, destination port, sequence number, acknowledgment number, flags
- UDP header: source port and destination port
from packet_analyzer.types import FiveTuple
tuple_ = FiveTuple(
parsed.src_ip,
parsed.dest_ip,
parsed.src_port,
parsed.dest_port,
parsed.protocol,
)What happens:
- The connection table is a dictionary:
FiveTuple -> Connection - If the tuple already exists, the existing connection is reused
- If not, a new connection is created
- All packets with the same 5-tuple share the same connection state
from packet_analyzer.extractors import extract_sni, extract_http_host, extract_dns_query
sni = extract_sni(parsed.payload)
host = extract_http_host(parsed.payload)
domain = extract_dns_query(parsed.payload)What happens:
- Check if the payload is a TLS Client Hello
- Skip through the handshake fields
- Find the SNI extension and extract the hostname
- For HTTP, extract the Host header
- For DNS, extract the queried domain name
reason = rules.should_block(src_ip, dst_port, app_type, domain)
if reason:
state = "BLOCKED"What happens in packet_analyzer/rules.py:
if src_ip in blocked_ips:
return BlockReason("IP", src_ip)
if dst_port in blocked_ports:
return BlockReason("PORT", str(dst_port))
if app in blocked_apps:
return BlockReason("APP", app.value)
if domain and is_domain_blocked(domain):
return BlockReason("DOMAIN", domain)
return Noneif reason:
dropped_packets += 1
# Don't write to output
else:
forwarded_packets += 1
# Write packet to output file
writer.write_packet(job)After processing all packets:
for app, count in app_counts.items():
print(f"{app}: {count}")Purpose: read and write network captures saved by Wireshark.
Key structures:
@dataclass
class PcapGlobalHeader:
magic_number: int
version_major: int
version_minor: int
thiszone: int
sigfigs: int
snaplen: int
network: int
raw: bytes
endian: str
@dataclass
class PcapPacketHeader:
ts_sec: int
ts_usec: int
incl_len: int
orig_len: intKey classes:
PcapReader: Opens the capture and yieldsRawPacketobjectsPcapWriter: Writes packets back to a PCAP file
Purpose: extract protocol fields from raw bytes.
Key function:
parsed = PacketParser.parse(raw)Important concepts:
Network byte order is handled with struct.unpack("!...") and ipaddress to decode values safely.
Purpose: extract domain names from TLS, HTTP, and DNS payloads.
For TLS (HTTPS):
sni = extract_sni(payload)For HTTP:
host = extract_http_host(payload)For DNS:
domain = extract_dns_query(payload)Purpose: define data structures used throughout the engine.
FiveTuple:
@dataclass(frozen=True)
class FiveTuple:
src_ip: str
dst_ip: str
src_port: int
dst_port: int
protocol: intAppType:
class AppType(Enum):
UNKNOWN = "Unknown"
HTTP = "HTTP"
HTTPS = "HTTPS"
DNS = "DNS"
GOOGLE = "Google"
FACEBOOK = "Facebook"
YOUTUBE = "YouTube"
# ... more appssni_to_app_type function:
def sni_to_app_type(sni: str) -> AppType:
if "youtube" in sni.lower():
return AppType.YOUTUBE
if "facebook" in sni.lower():
return AppType.FACEBOOK
# ... more patternsPurpose: store and evaluate blocking rules.
Key class:
rules = RuleManager()
rules.block_ip("192.168.1.50")
rules.block_app("YouTube")
rules.block_domain("*.example.com")
rules.block_port(443)When you visit https://www.youtube.com:
┌──────────┐ ┌──────────┐
│ Browser │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ ──── Client Hello ─────────────────────►│
│ (includes SNI: www.youtube.com) │
│ │
│ ◄─── Server Hello ───────────────────── │
│ (includes certificate) │
│ │
│ ──── Key Exchange ─────────────────────►│
│ │
│ ◄═══ Encrypted Data ══════════════════► │
│ (from here on, everything is │
│ encrypted - we can't see it) │
We can only extract SNI from the Client Hello.
Byte 0: Content Type = 0x16 (Handshake)
Bytes 1-2: Version = 0x0301 (TLS 1.0)
Bytes 3-4: Record Length
-- Handshake Layer --
Byte 5: Handshake Type = 0x01 (Client Hello)
Bytes 6-8: Handshake Length
-- Client Hello Body --
Bytes 9-10: Client Version
Bytes 11-42: Random (32 bytes)
Byte 43: Session ID Length (N)
Bytes 44 to 44+N: Session ID
... Cipher Suites ...
... Compression Methods ...
-- Extensions --
Bytes X-X+1: Extensions Length
For each extension:
Bytes: Extension Type (2)
Bytes: Extension Length (2)
Bytes: Extension Data
-- SNI Extension (Type 0x0000) --
Extension Type: 0x0000
Extension Length: L
SNI List Length: M
SNI Type: 0x00 (hostname)
SNI Length: K
SNI Value: "www.youtube.com" ← THE GOAL!
from packet_analyzer.extractors import extract_sni
sni = extract_sni(payload)
if sni:
print(sni)HTTP requests usually include a Host header:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: ...The Python extractor looks for that header and returns the host name.
| Rule Type | Example | What it Blocks |
|---|---|---|
| IP | 192.168.1.50 |
All traffic from this source |
| App | YouTube |
All YouTube connections |
| Domain | tiktok |
Any SNI containing "tiktok" |
| Port | 443 |
Any connection to that destination port |
Packet arrives
|
v
┌─────────────────────────────────┐
│ Is source IP in blocked list? │──Yes──► DROP
└───────────────┬─────────────────┘
│No
v
┌─────────────────────────────────┐
│ Is destination port blocked? │──Yes──► DROP
└───────────────┬─────────────────┘
│No
v
┌─────────────────────────────────┐
│ Is app type in blocked list? │──Yes──► DROP
└───────────────┬─────────────────┘
│No
v
┌─────────────────────────────────┐
│ Does domain match a rule? │──Yes──► DROP
└───────────────┬─────────────────┘
│No
v
FORWARD
Important: we block at the connection level, not packet level.
Connection to YouTube:
Packet 1 (SYN) -> No SNI yet, FORWARD
Packet 2 (SYN-ACK) -> No SNI yet, FORWARD
Packet 3 (ACK) -> No SNI yet, FORWARD
Packet 4 (Client Hello) -> SNI: www.youtube.com
-> App: YOUTUBE (blocked!)
-> Mark connection as BLOCKED
-> DROP this packet
Packet 5 (Data) -> Connection is BLOCKED -> DROP
Packet 6 (Data) -> Connection is BLOCKED -> DROP
...all subsequent packets -> DROP
Why this approach:
- We cannot identify the app until we see the Client Hello or Host header
- Once identified, we block all future packets of that connection
- The connection will fail or time out on the client side
- Python 3.10 or newer
- No external libraries are required for the core parser and DPI engine
python dpi_engine.py test_dpi.pcap filtered.pcappython dpi_engine.py test_dpi.pcap filtered.pcap \
--block-app YouTube \
--block-app TikTok \
--block-ip 192.168.1.50 \
--block-domain facebook \
--block-port 443python dpi_engine.py test_dpi.pcap filtered.pcap --rules rules.txtpython analyze_pcap.py test_dpi.pcappython generate_test_pcap.pyThe Python CLI accepts --lbs and --fps for compatibility with the original project layout, but the port runs as a single-process pipeline.
====================================
DPI ENGINE v1.0
Configuration: Load Balancers=2, FPs per LB=2 (Python port runs single-process)
[RuleManager] Blocked app: YouTube
[RuleManager] Blocked IP: 192.168.1.50
[DPIEngine] Processing: test_dpi.pcap
[DPIEngine] Output to: filtered.pcap
Opened PCAP file: test_dpi.pcap
Version: 2.4
Snaplen: 65535 bytes
Link type: 1 (Ethernet)
====================================
DPI ENGINE STATISTICS
====================================
Total Packets: 77
Total Bytes: 5738
TCP Packets: 73
UDP Packets: 4
Forwarded: 69
Dropped/Blocked: 8
Drop Rate: 10.39%
Connections: 21
Application Distribution:
HTTPS 16
Unknown 4
YouTube 2
DNS 1
Facebook 1
Top Domains:
www.youtube.com 2
www.facebook.com 1
www.google.com 1
| Section | Meaning |
|---|---|
| Configuration | CLI options used for the run |
| Rules | Which blocking rules are active |
| Total Packets | Packets read from input file |
| Forwarded | Packets written to output file |
| Dropped/Blocked | Packets blocked and not written |
| Connections | Unique five-tuples tracked |
| Application Distribution | Traffic classification results |
| Top Domains | Actual domains found in payloads |
-
Add more app signatures in packet_analyzer/types.py
-
Add bandwidth throttling instead of immediate dropping in packet_analyzer/dpi_engine.py
-
Add live statistics updates in a background thread if you want to evolve the Python port into a streaming engine
-
Add QUIC/HTTP3 support for UDP-based HTTPS traffic
-
Add persistent rules loading and saving
This Python DPI engine demonstrates:
- Network protocol parsing - understanding packet structure
- Deep packet inspection - looking inside TLS, HTTP, and DNS payloads
- Flow tracking - managing stateful connections
- Rule-based filtering - blocking traffic by IP, app, domain, or port
- Python packet processing - reading, classifying, and writing PCAP files
The key insight is that even HTTPS traffic can leak useful metadata in the TLS handshake, allowing network operators to identify and control application usage.
If you have questions about any part of this project, the code is organized to follow the same flow described in this document. Start with analyze_pcap.py to understand packet parsing, then move to packet_analyzer/dpi_engine.py to see how filtering and reporting work.
Happy learning!