tiny-dpi-engine
Лёгкий движок глубокого инспектирования пакетов (Deep Packet Inspection) на C11.
Захватывает сетевой трафик, разбирает пакеты, классифицирует протоколы с помощью сигнатур (алгоритм Ахо-Корасик) и портовых эвристик, отслеживает сетевые потоки в реальном времени.
Репозиторий: gitflic.ru/project/wirewalk/tiny-dpi-engine | Зеркало: github.com/wirewalk/tiny-dpi-engine
Архитектура
┌─────────────┐ ┌───────────┐ ┌────────────────┐ ┌──────────────┐
│ Захват │───>│ Разбор │───>│ Классификация │───>│ Таблица │
│ (libpcap) │ │ пакетов │ │ протоколов │ │ потоков │
└─────────────┘ └───────────┘ └────────────────┘ └──────────────┘
│ │ │
v v v
Ethernet/IPv4 Ахо-Корасик + Хэш-таблица
TCP/UDP/ICMP эвристики payload 5-кортежей
+ фолбэк на порты
Компоненты
| Компонент | Файл | Назначение |
|---|---|---|
| Парсер пакетов | src/packet.c |
Разбор Ethernet (включая 802.1Q VLAN), IPv4, TCP, UDP, ICMP |
| База сигнатур | src/signatures.c |
Загрузка сигнатур из текстового файла |
| Ахо-Корасик | src/aho_corasick.c |
Множественный поиск подстрок за O(n) |
| Классификатор | src/classify.c |
3-стадийная классификация: payload → сигнатуры → порты |
| Таблица потоков | src/flow.c |
Хэш-таблица с канонизацией 5-кортежа |
| Захват трафика | src/capture.c |
Живой захват (libpcap) и чтение pcap-файлов |
| Статистика | src/stats.c |
Агрегированные метрики по протоколам |
Ключевые алгоритмы
Ахо-Корасик
Алгоритм множественного поиска подстрок. Построение автомата состоит из двух фаз:
-
Trie-дерево: все сигнатуры вставляются в префиксное дерево. Каждый узел — массив из 256 переходов (полный байтовый алфавит). Лист содержит
sig_id— идентификатор совпавшей сигнатуры. -
Failure-ссылки (BFS): для каждого узла вычисляется самая длинная суффиксная цепочка, которая также является путём от корня. При несовпадении текущего байта — переход по failure-ссылке вместо возврата к началу.
Сигнатуры: "GET ", "SSH-", "POST"
root
/ | \
G S P
| | |
E S O
| | |
T H S
| | |
' ' - T
Для узла 'H' (SSH): fail → root (нет другого суффикса)
Для узла 'E' (GET): fail → root
Поиск: один проход по payload, O(n) по длине данных. Не зависит от количества сигнатур.
Реализация использует пул аллокаций — все узлы в одном непрерывном массиве. Это улучшает локальность кэша при обходе дерева и упрощает освобождение памяти (один free).
Хэш-таблица потоков
Ключ — канонический 5-кортеж (src_ip, dst_ip, src_port, dst_port, ip_proto).
Канонизация: IP-адреса упорядочиваются по числовому значению (меньший → src). Это гарантирует, что пакеты A→B и B→A попадают в один поток. При равенстве IP — упорядочиваются порты.
Пакет A→B: (10.0.0.2:54321 → 10.0.0.1:80, TCP)
Канонический ключ: (10.0.0.1:80, 10.0.0.2:54321, TCP)
Пакет B→A: (10.0.0.1:80 → 10.0.0.2:54321, TCP)
Канонический ключ: (10.0.0.1:80, 10.0.0.2:54321, TCP) ← тот же!
Хэш-функция: XOR полей + мультипликативное перемешивание (Murmur-like finalizer). Размер таблицы — простое число (65537) для равномерного распределения. Разрешение коллизий — цепочечное (chaining).
Классификация протоколов
Трёхстадийная стратегия, привязанная к потоку:
Пакет → Поиск/создание потока
│
├─ Поток уже классифицирован? → Наследуем результат, пропускаем анализ
│
├─ Стадия 1: Инспекция payload
│ ├─ HTTP: "GET ", "POST ", "HTTP/1.x"
│ ├─ SSH: "SSH-2.0", "SSH-1.99"
│ ├─ SIP: "INVITE ", "BYE ", "REGISTER ", "SIP/2.0"
│ ├─ TLS: ContentType=0x16, Version=0x03xx
│ ├─ QUIC: "Q04x" (версия)
│ └─ RTP: version=2, payload_type 0..34 (аудио/видео)
│ или 200..207 (RTCP: SR, RR, SDES, BYE)
│
├─ Стадия 2: Ахо-Корасик (пользовательские сигнатуры)
│
└─ Стадия 3: Фолбэк на well-known порты
├─ 80=HTTP, 443=HTTPS, 53=DNS, 22=SSH
├─ 21=FTP, 25=SMTP, 5060=SIP, 123=NTP
└─ 67/68=DHCP
Stateful-подход: если первый пакет потока классифицирован, все последующие наследуют результат. Это исключает повторный анализ и повышает производительность.
Формат сигнатур
Текстовый файл, одна сигнатура на строку:
# Комментарии начинаются с #
ПРОТОКОЛ: шаблон_поиска
# Примеры:
HTTP: GET
HTTP: POST
SSH: SSH-2.0
TLS: \x16\x03\x01
SIP: INVITE
SIP: BYE
QUIC: Q050
Правила:
ПРОТОКОЛ— один из поддерживаемых: HTTP, HTTPS, DNS, SSH, FTP, SMTP, RTP, SIP, RTCP, QUIC, DHCP, TLSшаблон_поиска— последовательность байтов (текст или\xHH)- Пробел после двоеточия необязателен
- Строки с неизвестным протоколом — пропускаются
Сборка и запуск
make # сборка (автоопределение libpcap)
make test # сборка + запуск unit-тестов
make clean # очистка
# Для live-capture нужна libpcap:
# Debian/Ubuntu: sudo apt install libpcap-dev
# Fedora: sudo dnf install libpcap-devel
# Alt Linux: sudo apt-get install libpcap-devel
Без libpcap проект собирается с заглушками — парсинг, классификация и flow tracking работают, захват отключён.
Примеры использования
# Живой захват с интерфейса (требует root)
sudo ./tiny-dpi-engine -i eth0 -n 1000
# Читать из pcap-файла
./tiny-dpi-engine -r capture.pcap
# С фильтром BPF и своей базой сигнатур
./tiny-dpi-engine -i any -f "port 53 or port 80" -s my_sigs.sig -n 5000
# Все опции
./tiny-dpi-engine -h
Ожидаемый вывод
tiny-dpi-engine
Loading signatures from: signatures/default.sig
Loaded 26 signatures
Aho-Corasick: 187 nodes
Capturing on eth0 (max 1000 packets, Ctrl+C to stop)...
=== DPI Statistics ===
Total packets: 1000
Total bytes: 523847
Classified: 947
Unclassified: 53
Active flows: 42
Classification: 94.7%
Per-protocol breakdown:
DNS 312
TLS 287
HTTP 198
HTTPS 84
SSH 31
QUIC 22
RTP 13
SRC IP PORT -> DST IP PORT PROTO PACKETS BYTES SIG
192.168.1.100 54321 -> 93.184.216.34 80 HTTP 15 8472
192.168.1.100 54322 -> 140.82.121.4 443 HTTPS 22 14891
Как добавить свой протокол
- Определить enum в
include/dpi.h— добавить значение вdpi_protocol_t - Имя протокола — добавить запись в массив
proto_namesвsrc/proto.c - Порт — добавить в функцию
classify_by_portsвsrc/classify.c - Payload-эвристика — добавить проверку в
classify_by_payloadвsrc/classify.c - Сигнатура — добавить строку в
signatures/default.sig
Пример: добавляем MQTT (порт 1883, стартовый байт 0x10):
// dpi.h
DPI_PROTO_MQTT,
// classify.c — classify_by_ports
if (dst == 1883 || src == 1883) return DPI_PROTO_MQTT;
// classify.c — classify_by_payload
if (payload_len >= 2 && (payload[0] & 0xF0) == 0x10) return DPI_PROTO_MQTT;
// signatures/default.sig
MQTT: \x10
Ограничения
Текущая реализация сознательно не включает:
- IPv6 — только IPv4 (ethertype 0x0800)
- IP-дефрагментацию — фрагментированные пакеты анализируются отдельно, только первый фрагмент содержит TCP/UDP-заголовки
- TCP reassembly — каждый сегмент обрабатывается независимо, payload из разных сегментов не склеивается
- Шифрованный трафик — TLS/QUIC определяются по заголовку, но содержимое не анализируется
- Декодирование протоколов — сигнатуры ищутся в raw payload, без декодирования Base64, gzip и т.д.
Это осознанный выбор: проект ориентирован на простоту и образовательную ценность, а не на production-полноту. Каждый из этих пунктов — потенциальная тема для отдельной статьи.
Лицензия
MIT
Крафтовое ПО. Не энтерпрайз. Не фреймворк. Вкусно.