netreplay 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netreplay-0.1.0/LICENSE +21 -0
- netreplay-0.1.0/PKG-INFO +167 -0
- netreplay-0.1.0/README.md +143 -0
- netreplay-0.1.0/gui/__init__.py +10 -0
- netreplay-0.1.0/gui/api.py +81 -0
- netreplay-0.1.0/gui/components/__init__.py +1 -0
- netreplay-0.1.0/gui/components/details.py +71 -0
- netreplay-0.1.0/gui/components/flow_list.py +44 -0
- netreplay-0.1.0/gui/components/header.py +81 -0
- netreplay-0.1.0/gui/components/timeline.py +154 -0
- netreplay-0.1.0/gui/main.py +219 -0
- netreplay-0.1.0/netreplay/__init__.py +6 -0
- netreplay-0.1.0/netreplay/api/__init__.py +4 -0
- netreplay-0.1.0/netreplay/api/app.py +62 -0
- netreplay-0.1.0/netreplay/api/routes/__init__.py +14 -0
- netreplay-0.1.0/netreplay/api/routes/capture.py +90 -0
- netreplay-0.1.0/netreplay/api/routes/flows.py +76 -0
- netreplay-0.1.0/netreplay/api/routes/packets.py +41 -0
- netreplay-0.1.0/netreplay/api/routes/sessions.py +60 -0
- netreplay-0.1.0/netreplay/api/schemas.py +89 -0
- netreplay-0.1.0/netreplay/api/websocket.py +51 -0
- netreplay-0.1.0/netreplay/cli/__init__.py +4 -0
- netreplay-0.1.0/netreplay/cli/main.py +317 -0
- netreplay-0.1.0/netreplay/core/__init__.py +10 -0
- netreplay-0.1.0/netreplay/core/capture/__init__.py +10 -0
- netreplay-0.1.0/netreplay/core/capture/base.py +41 -0
- netreplay-0.1.0/netreplay/core/capture/pcap_backend.py +50 -0
- netreplay-0.1.0/netreplay/core/capture/scapy_backend.py +119 -0
- netreplay-0.1.0/netreplay/core/flows/models.py +71 -0
- netreplay-0.1.0/netreplay/core/flows/tracker.py +97 -0
- netreplay-0.1.0/netreplay/core/packets/models.py +51 -0
- netreplay-0.1.0/netreplay/core/packets/parser.py +130 -0
- netreplay-0.1.0/netreplay/core/protocols/__init__.py +1 -0
- netreplay-0.1.0/netreplay/core/protocols/decrypt.py +291 -0
- netreplay-0.1.0/netreplay/core/protocols/decrypt_service.py +115 -0
- netreplay-0.1.0/netreplay/core/protocols/dns.py +101 -0
- netreplay-0.1.0/netreplay/core/protocols/tls.py +145 -0
- netreplay-0.1.0/netreplay/core/replay/__init__.py +4 -0
- netreplay-0.1.0/netreplay/core/service.py +277 -0
- netreplay-0.1.0/netreplay/core/storage/__init__.py +16 -0
- netreplay-0.1.0/netreplay/core/storage/database.py +499 -0
- netreplay-0.1.0/netreplay/core/storage/nrp.py +49 -0
- netreplay-0.1.0/netreplay/core/timeline/__init__.py +14 -0
- netreplay-0.1.0/netreplay/core/timeline/service.py +132 -0
- netreplay-0.1.0/netreplay.egg-info/PKG-INFO +167 -0
- netreplay-0.1.0/netreplay.egg-info/SOURCES.txt +60 -0
- netreplay-0.1.0/netreplay.egg-info/dependency_links.txt +1 -0
- netreplay-0.1.0/netreplay.egg-info/entry_points.txt +2 -0
- netreplay-0.1.0/netreplay.egg-info/requires.txt +12 -0
- netreplay-0.1.0/netreplay.egg-info/top_level.txt +3 -0
- netreplay-0.1.0/pyproject.toml +42 -0
- netreplay-0.1.0/setup.cfg +4 -0
- netreplay-0.1.0/tests/test_api.py +106 -0
- netreplay-0.1.0/tests/test_flows.py +100 -0
- netreplay-0.1.0/tests/test_nrp.py +58 -0
- netreplay-0.1.0/tests/test_parser.py +171 -0
- netreplay-0.1.0/tests/test_pcap_import.py +57 -0
- netreplay-0.1.0/tests/test_pcap_import_decrypt.py +85 -0
- netreplay-0.1.0/tests/test_pipeline.py +129 -0
- netreplay-0.1.0/tests/test_storage.py +101 -0
- netreplay-0.1.0/tests/test_timeline.py +123 -0
- netreplay-0.1.0/tests/test_tls_decrypt.py +118 -0
netreplay-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NetReplay contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
netreplay-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: netreplay
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NetReplay - network traffic time machine. Capture, analyze and replay network traffic as a story.
|
|
5
|
+
Author: NetReplay
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arbuztratil-design/NetReplay
|
|
8
|
+
Project-URL: Repository, https://github.com/arbuztratil-design/NetReplay
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: scapy>=2.5
|
|
13
|
+
Requires-Dist: cryptography>=42
|
|
14
|
+
Requires-Dist: fastapi>=0.110
|
|
15
|
+
Requires-Dist: uvicorn>=0.29
|
|
16
|
+
Requires-Dist: websockets>=12.0
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Requires-Dist: flet>=0.86
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# NetReplay
|
|
26
|
+
|
|
27
|
+
Машина времени для сетевого трафика. Захват → разбор → анализ потоков →
|
|
28
|
+
временная линия → «перемотка» событий обратно.
|
|
29
|
+
|
|
30
|
+
MVP: `netreplay capture` собирает трафик в файл `.nrp`, `netreplay timeline`
|
|
31
|
+
показывает его как читаемую историю, `netreplay replay` воспроизводит события
|
|
32
|
+
с реалистичными паузами, а `netreplay serve` + `netreplay gui` дают REST/WebSocket
|
|
33
|
+
API и оконный клиент.
|
|
34
|
+
|
|
35
|
+
## Возможности
|
|
36
|
+
|
|
37
|
+
- Захват пакетов через Scapy (на Windows требуется Npcap).
|
|
38
|
+
- Разбор Ethernet / IPv4 / IPv6 / TCP / UDP / ARP / DNS / TLS: SNI, версии TLS,
|
|
39
|
+
DNS query/response.
|
|
40
|
+
- Офлайн-расшифровка TLS 1.2 (AES-128/256-GCM, PRF SHA-256/SHA-384) из
|
|
41
|
+
внешнего SSLKEYLOGFILE: расшифрованные прикладные данные попадают в таймлайн
|
|
42
|
+
как события `DECRYPT`.
|
|
43
|
+
- Потоки: нормализованный 5-tuple, TCP state machine
|
|
44
|
+
(SYN → SYN/ACK → ESTABLISHED → FIN → CLOSED, RST).
|
|
45
|
+
- Timeline-события: старт потока, переходы TCP, DNS, TLS, DECRYPT — с фильтрами
|
|
46
|
+
по времени, типам и потоку.
|
|
47
|
+
- Собственный формат `.nrp` — versioned SQLite (magic `NREP`, v1), WAL,
|
|
48
|
+
payload отдельными 64 КБ-чанками в `raw_blocks`.
|
|
49
|
+
- API: REST + WebSocket (live-события захвата), GUI как чистый клиент API.
|
|
50
|
+
|
|
51
|
+
## Установка
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
pip install -e .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
На Windows для живого захвата установите [Npcap](https://npcap.com)
|
|
58
|
+
(с опцией «WinPcap API–compatible Mode»). Без него `netreplay capture`
|
|
59
|
+
завершится с понятной ошибкой.
|
|
60
|
+
|
|
61
|
+
## Использование
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
# Список интерфейсов
|
|
65
|
+
netreplay interfaces
|
|
66
|
+
|
|
67
|
+
# Живой захват 10 секунд
|
|
68
|
+
netreplay capture -i "Ethernet" -o capture.nrp -d 10
|
|
69
|
+
|
|
70
|
+
# Метаданные, потоки и временная линия
|
|
71
|
+
netreplay inspect capture.nrp
|
|
72
|
+
netreplay flows capture.nrp
|
|
73
|
+
netreplay timeline capture.nrp
|
|
74
|
+
netreplay timeline capture.nrp --types TCP,TLS --limit 50
|
|
75
|
+
|
|
76
|
+
# Исторический реплей с ускорением x50
|
|
77
|
+
netreplay replay capture.nrp --speed 50
|
|
78
|
+
|
|
79
|
+
# Список сессий в workspace (переменная окружения NETREPLAY_WORKSPACE,
|
|
80
|
+
# по умолчанию ./netreplay_data)
|
|
81
|
+
netreplay sessions
|
|
82
|
+
|
|
83
|
+
# Импорт и анализ существующего PCAP/PCAPNG без захвата
|
|
84
|
+
netreplay import-pcap dump.pcap -o dump.nrp
|
|
85
|
+
|
|
86
|
+
# REST + WebSocket API и GUI
|
|
87
|
+
netreplay serve # http://127.0.0.1:8000, документация /docs
|
|
88
|
+
netreplay gui # в отдельном терминале; подключится к API
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Быстрая проверка без сети: в папке `examples/` лежит готовый `demo.nrp`
|
|
92
|
+
(11 пакетов: DNS, TCP handshake, TLS, завершение соединения).
|
|
93
|
+
|
|
94
|
+
```powershell
|
|
95
|
+
netreplay timeline examples/demo.nrp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Офлайн-импорт: `netreplay import-pcap` прогоняет `.pcap`/`.pcapng` через
|
|
99
|
+
тот же пайплайн (разбор → потоки → события → `.nrp`), что и живой захват.
|
|
100
|
+
Результат открывается любыми обычными командами: `flows`, `timeline`,
|
|
101
|
+
`replay`, `serve`/`gui`.
|
|
102
|
+
|
|
103
|
+
Расшифровка TLS в офлайн-режиме: если передан файл ключей SSLKEYLOGFILE
|
|
104
|
+
(строки `CLIENT_RANDOM`), TLS 1.2 соединения (AES-128/256-GCM, SHA-256/SHA-384)
|
|
105
|
+
расшифровываются и публикуются в таймлайн как события `DECRYPT` с
|
|
106
|
+
превью прикладных данных (например, начала HTTP-запроса/ответа). Такой
|
|
107
|
+
файл умеют отдавать curl, OpenSSL и браузеры через переменную окружения
|
|
108
|
+
`SSLKEYLOGFILE`.
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
netreplay import-pcap dump.pcap -o dump.nrp --keylog keys.log
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Архитектура
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
netreplay/
|
|
118
|
+
core/ # вся логика, без наружных зависимостей
|
|
119
|
+
packets/parser.py # Scapy -> ParsedPacket
|
|
120
|
+
protocols/dns.py, tls.py
|
|
121
|
+
protocols/decrypt.py # TLS 1.2 AES-GCM расшифровка (keylog) + key-block PRF
|
|
122
|
+
protocols/decrypt_service.py # пост-проход по .nrp -> события DECRYPT
|
|
123
|
+
flows/tracker.py # нормализация 5-tuple, TCP state machine
|
|
124
|
+
storage/database.py # SQLite-хранилище сессий
|
|
125
|
+
storage/nrp.py # формат .nrp: magic, версия, чанки
|
|
126
|
+
timeline/service.py # события + timeline + replay
|
|
127
|
+
capture/scapy_backend.py
|
|
128
|
+
capture/pcap_backend.py # офлайн-источник: PCAP/PCAPNG -> CapturedPacket
|
|
129
|
+
service.py # CaptureController, NetReplayService, import_pcap
|
|
130
|
+
api/ # FastAPI: REST + WebSocket, схемы
|
|
131
|
+
cli/main.py # Typer-команды (используют Core)
|
|
132
|
+
gui/ # Flet-клиент, данные только через API
|
|
133
|
+
tests/ # pytest (Windows: + реальный TLS 1.2 handshake
|
|
134
|
+
# через OpenSSL DLL и проверка расшифровки)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Правила разделения:
|
|
138
|
+
- GUI не трогает захват, хранилище и внутренние структуры — только API.
|
|
139
|
+
- CLI не дублирует логику Core (разбор, потоки, timeline) — только вызывает её.
|
|
140
|
+
- Core не зависит от FastAPI/Typer/Flet.
|
|
141
|
+
- `PcapBackend` реализует тот же `CaptureBackend`, что и `ScapyBackend`,
|
|
142
|
+
поэтому офлайн-импорт переиспользует пайплайн без изменений.
|
|
143
|
+
|
|
144
|
+
## API
|
|
145
|
+
|
|
146
|
+
- `GET /sessions`, `GET /sessions/{id}`, `GET /sessions/{id}/timeline`,
|
|
147
|
+
`GET /sessions/{id}/flows`
|
|
148
|
+
- `GET /flows/{id}`, `GET /packets/{id}` (с `?raw=true` — hex payload)
|
|
149
|
+
- `POST /capture/start`, `POST /capture/stop`, `GET /capture/status`,
|
|
150
|
+
`GET /interfaces`
|
|
151
|
+
- `WS /ws` — пульс и live-события захвата:
|
|
152
|
+
`{"type":"event","timestamp":...,"flow_id":...,"protocol":"TCP","summary":"..."}`
|
|
153
|
+
|
|
154
|
+
## Тестирование
|
|
155
|
+
|
|
156
|
+
```powershell
|
|
157
|
+
python -m pytest tests -q
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Roadmap (после MVP)
|
|
161
|
+
|
|
162
|
+
- ~~Офлайн-анализ существующих PCAP без захвата~~ — `netreplay import-pcap`.
|
|
163
|
+
- ~~Отложенная расшифровка TLS (внешний кейлог-файл)~~ — `--keylog`.
|
|
164
|
+
- TLS 1.3, CBC/ChaCha20-сьюты, проверка Finished-сообщений.
|
|
165
|
+
- Перехват и обратная инъекция пакетов (replay-out).
|
|
166
|
+
- Векторы похожести/поиск по домену и IP в `inspect`.
|
|
167
|
+
- Модульный CLI-бэкенд (Mock/PCAP-файл) без изменения ядра.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# NetReplay
|
|
2
|
+
|
|
3
|
+
Машина времени для сетевого трафика. Захват → разбор → анализ потоков →
|
|
4
|
+
временная линия → «перемотка» событий обратно.
|
|
5
|
+
|
|
6
|
+
MVP: `netreplay capture` собирает трафик в файл `.nrp`, `netreplay timeline`
|
|
7
|
+
показывает его как читаемую историю, `netreplay replay` воспроизводит события
|
|
8
|
+
с реалистичными паузами, а `netreplay serve` + `netreplay gui` дают REST/WebSocket
|
|
9
|
+
API и оконный клиент.
|
|
10
|
+
|
|
11
|
+
## Возможности
|
|
12
|
+
|
|
13
|
+
- Захват пакетов через Scapy (на Windows требуется Npcap).
|
|
14
|
+
- Разбор Ethernet / IPv4 / IPv6 / TCP / UDP / ARP / DNS / TLS: SNI, версии TLS,
|
|
15
|
+
DNS query/response.
|
|
16
|
+
- Офлайн-расшифровка TLS 1.2 (AES-128/256-GCM, PRF SHA-256/SHA-384) из
|
|
17
|
+
внешнего SSLKEYLOGFILE: расшифрованные прикладные данные попадают в таймлайн
|
|
18
|
+
как события `DECRYPT`.
|
|
19
|
+
- Потоки: нормализованный 5-tuple, TCP state machine
|
|
20
|
+
(SYN → SYN/ACK → ESTABLISHED → FIN → CLOSED, RST).
|
|
21
|
+
- Timeline-события: старт потока, переходы TCP, DNS, TLS, DECRYPT — с фильтрами
|
|
22
|
+
по времени, типам и потоку.
|
|
23
|
+
- Собственный формат `.nrp` — versioned SQLite (magic `NREP`, v1), WAL,
|
|
24
|
+
payload отдельными 64 КБ-чанками в `raw_blocks`.
|
|
25
|
+
- API: REST + WebSocket (live-события захвата), GUI как чистый клиент API.
|
|
26
|
+
|
|
27
|
+
## Установка
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
На Windows для живого захвата установите [Npcap](https://npcap.com)
|
|
34
|
+
(с опцией «WinPcap API–compatible Mode»). Без него `netreplay capture`
|
|
35
|
+
завершится с понятной ошибкой.
|
|
36
|
+
|
|
37
|
+
## Использование
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
# Список интерфейсов
|
|
41
|
+
netreplay interfaces
|
|
42
|
+
|
|
43
|
+
# Живой захват 10 секунд
|
|
44
|
+
netreplay capture -i "Ethernet" -o capture.nrp -d 10
|
|
45
|
+
|
|
46
|
+
# Метаданные, потоки и временная линия
|
|
47
|
+
netreplay inspect capture.nrp
|
|
48
|
+
netreplay flows capture.nrp
|
|
49
|
+
netreplay timeline capture.nrp
|
|
50
|
+
netreplay timeline capture.nrp --types TCP,TLS --limit 50
|
|
51
|
+
|
|
52
|
+
# Исторический реплей с ускорением x50
|
|
53
|
+
netreplay replay capture.nrp --speed 50
|
|
54
|
+
|
|
55
|
+
# Список сессий в workspace (переменная окружения NETREPLAY_WORKSPACE,
|
|
56
|
+
# по умолчанию ./netreplay_data)
|
|
57
|
+
netreplay sessions
|
|
58
|
+
|
|
59
|
+
# Импорт и анализ существующего PCAP/PCAPNG без захвата
|
|
60
|
+
netreplay import-pcap dump.pcap -o dump.nrp
|
|
61
|
+
|
|
62
|
+
# REST + WebSocket API и GUI
|
|
63
|
+
netreplay serve # http://127.0.0.1:8000, документация /docs
|
|
64
|
+
netreplay gui # в отдельном терминале; подключится к API
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Быстрая проверка без сети: в папке `examples/` лежит готовый `demo.nrp`
|
|
68
|
+
(11 пакетов: DNS, TCP handshake, TLS, завершение соединения).
|
|
69
|
+
|
|
70
|
+
```powershell
|
|
71
|
+
netreplay timeline examples/demo.nrp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Офлайн-импорт: `netreplay import-pcap` прогоняет `.pcap`/`.pcapng` через
|
|
75
|
+
тот же пайплайн (разбор → потоки → события → `.nrp`), что и живой захват.
|
|
76
|
+
Результат открывается любыми обычными командами: `flows`, `timeline`,
|
|
77
|
+
`replay`, `serve`/`gui`.
|
|
78
|
+
|
|
79
|
+
Расшифровка TLS в офлайн-режиме: если передан файл ключей SSLKEYLOGFILE
|
|
80
|
+
(строки `CLIENT_RANDOM`), TLS 1.2 соединения (AES-128/256-GCM, SHA-256/SHA-384)
|
|
81
|
+
расшифровываются и публикуются в таймлайн как события `DECRYPT` с
|
|
82
|
+
превью прикладных данных (например, начала HTTP-запроса/ответа). Такой
|
|
83
|
+
файл умеют отдавать curl, OpenSSL и браузеры через переменную окружения
|
|
84
|
+
`SSLKEYLOGFILE`.
|
|
85
|
+
|
|
86
|
+
```powershell
|
|
87
|
+
netreplay import-pcap dump.pcap -o dump.nrp --keylog keys.log
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Архитектура
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
netreplay/
|
|
94
|
+
core/ # вся логика, без наружных зависимостей
|
|
95
|
+
packets/parser.py # Scapy -> ParsedPacket
|
|
96
|
+
protocols/dns.py, tls.py
|
|
97
|
+
protocols/decrypt.py # TLS 1.2 AES-GCM расшифровка (keylog) + key-block PRF
|
|
98
|
+
protocols/decrypt_service.py # пост-проход по .nrp -> события DECRYPT
|
|
99
|
+
flows/tracker.py # нормализация 5-tuple, TCP state machine
|
|
100
|
+
storage/database.py # SQLite-хранилище сессий
|
|
101
|
+
storage/nrp.py # формат .nrp: magic, версия, чанки
|
|
102
|
+
timeline/service.py # события + timeline + replay
|
|
103
|
+
capture/scapy_backend.py
|
|
104
|
+
capture/pcap_backend.py # офлайн-источник: PCAP/PCAPNG -> CapturedPacket
|
|
105
|
+
service.py # CaptureController, NetReplayService, import_pcap
|
|
106
|
+
api/ # FastAPI: REST + WebSocket, схемы
|
|
107
|
+
cli/main.py # Typer-команды (используют Core)
|
|
108
|
+
gui/ # Flet-клиент, данные только через API
|
|
109
|
+
tests/ # pytest (Windows: + реальный TLS 1.2 handshake
|
|
110
|
+
# через OpenSSL DLL и проверка расшифровки)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Правила разделения:
|
|
114
|
+
- GUI не трогает захват, хранилище и внутренние структуры — только API.
|
|
115
|
+
- CLI не дублирует логику Core (разбор, потоки, timeline) — только вызывает её.
|
|
116
|
+
- Core не зависит от FastAPI/Typer/Flet.
|
|
117
|
+
- `PcapBackend` реализует тот же `CaptureBackend`, что и `ScapyBackend`,
|
|
118
|
+
поэтому офлайн-импорт переиспользует пайплайн без изменений.
|
|
119
|
+
|
|
120
|
+
## API
|
|
121
|
+
|
|
122
|
+
- `GET /sessions`, `GET /sessions/{id}`, `GET /sessions/{id}/timeline`,
|
|
123
|
+
`GET /sessions/{id}/flows`
|
|
124
|
+
- `GET /flows/{id}`, `GET /packets/{id}` (с `?raw=true` — hex payload)
|
|
125
|
+
- `POST /capture/start`, `POST /capture/stop`, `GET /capture/status`,
|
|
126
|
+
`GET /interfaces`
|
|
127
|
+
- `WS /ws` — пульс и live-события захвата:
|
|
128
|
+
`{"type":"event","timestamp":...,"flow_id":...,"protocol":"TCP","summary":"..."}`
|
|
129
|
+
|
|
130
|
+
## Тестирование
|
|
131
|
+
|
|
132
|
+
```powershell
|
|
133
|
+
python -m pytest tests -q
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Roadmap (после MVP)
|
|
137
|
+
|
|
138
|
+
- ~~Офлайн-анализ существующих PCAP без захвата~~ — `netreplay import-pcap`.
|
|
139
|
+
- ~~Отложенная расшифровка TLS (внешний кейлог-файл)~~ — `--keylog`.
|
|
140
|
+
- TLS 1.3, CBC/ChaCha20-сьюты, проверка Finished-сообщений.
|
|
141
|
+
- Перехват и обратная инъекция пакетов (replay-out).
|
|
142
|
+
- Векторы похожести/поиск по домену и IP в `inspect`.
|
|
143
|
+
- Модульный CLI-бэкенд (Mock/PCAP-файл) без изменения ядра.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Flet GUI - the NetReplay desktop client.
|
|
2
|
+
|
|
3
|
+
The GUI is a pure API client: it does not parse packets, capture traffic or
|
|
4
|
+
touch the store. All data comes from the NetReplay REST API + WebSocket.
|
|
5
|
+
"""
|
|
6
|
+
from gui.main import main
|
|
7
|
+
|
|
8
|
+
import netreplay # noqa: F401 (ensure package is on path)
|
|
9
|
+
|
|
10
|
+
__all__ = ["main"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Thin HTTP client for the NetReplay API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NetReplayClient:
|
|
14
|
+
def __init__(self, base_url: str):
|
|
15
|
+
self.base_url = base_url.rstrip("/")
|
|
16
|
+
self._client = httpx.Client(
|
|
17
|
+
timeout=httpx.Timeout(10.0, connect=3.0), trust_env=False
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def _get(self, path: str, **params: Any) -> Any:
|
|
21
|
+
try:
|
|
22
|
+
resp = self._client.get(f"{self.base_url}{path}", params=params)
|
|
23
|
+
resp.raise_for_status()
|
|
24
|
+
return resp.json()
|
|
25
|
+
except httpx.HTTPError as exc:
|
|
26
|
+
raise ApiError(f"GET {path}: {exc}") from exc
|
|
27
|
+
|
|
28
|
+
def _post(self, path: str, body: Any | None = None) -> Any:
|
|
29
|
+
try:
|
|
30
|
+
resp = self._client.post(f"{self.base_url}{path}", json=body or {})
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
return resp.json()
|
|
33
|
+
except httpx.HTTPError as exc:
|
|
34
|
+
raise ApiError(f"POST {path}: {exc}") from exc
|
|
35
|
+
|
|
36
|
+
def interfaces(self) -> list[dict]:
|
|
37
|
+
return self._get("/api/interfaces")
|
|
38
|
+
|
|
39
|
+
def sessions(self) -> list[dict]:
|
|
40
|
+
return self._get("/api/sessions")
|
|
41
|
+
|
|
42
|
+
def session(self, session_id: str) -> dict:
|
|
43
|
+
return self._get(f"/api/sessions/{session_id}")
|
|
44
|
+
|
|
45
|
+
def timeline(
|
|
46
|
+
self,
|
|
47
|
+
session_id: str,
|
|
48
|
+
start: float | None = None,
|
|
49
|
+
end: float | None = None,
|
|
50
|
+
flow_id: int | None = None,
|
|
51
|
+
limit: int = 2000,
|
|
52
|
+
) -> list[dict]:
|
|
53
|
+
params = {}
|
|
54
|
+
if start is not None:
|
|
55
|
+
params["start"] = start
|
|
56
|
+
if end is not None:
|
|
57
|
+
params["end"] = end
|
|
58
|
+
if flow_id is not None:
|
|
59
|
+
params["flow_id"] = flow_id
|
|
60
|
+
params["limit"] = limit
|
|
61
|
+
return self._get(f"/api/sessions/{session_id}/timeline", **params)
|
|
62
|
+
|
|
63
|
+
def flows(self, session_id: str) -> list[dict]:
|
|
64
|
+
return self._get(f"/api/sessions/{session_id}/flows")
|
|
65
|
+
|
|
66
|
+
def flow(self, flow_id: int, include_packets: bool = True) -> dict:
|
|
67
|
+
return self._get(
|
|
68
|
+
"/api/flows/{}".format(flow_id), include_packets="true" if include_packets else "false"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def packet(self, packet_id: int, raw: bool = False) -> dict:
|
|
72
|
+
return self._get("/api/packets/{}".format(packet_id), raw="true" if raw else "false")
|
|
73
|
+
|
|
74
|
+
def capture_start(self, interface: str) -> dict:
|
|
75
|
+
return self._post("/api/capture/start", {"interface": interface})
|
|
76
|
+
|
|
77
|
+
def capture_stop(self) -> dict:
|
|
78
|
+
return self._post("/api/capture/stop")
|
|
79
|
+
|
|
80
|
+
def capture_status(self) -> dict:
|
|
81
|
+
return self._get("/api/capture/status")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reusable GUI components."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Event / flow details widget."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import flet as ft
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _fmt(ts: float) -> str:
|
|
10
|
+
local = time.localtime(ts)
|
|
11
|
+
return time.strftime("%H:%M:%S", local) + f".{int((ts % 1) * 1000):03d}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DetailsPanel:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self._header = ft.Text("Details", size=14, weight=ft.FontWeight.BOLD)
|
|
17
|
+
self._event = ft.Text("", size=12, color=ft.Colors.BLUE_200, selectable=True)
|
|
18
|
+
self._flow = ft.Text("", size=12, color=ft.Colors.TEAL_200, selectable=True)
|
|
19
|
+
self._body = ft.ListView(expand=True, spacing=2, padding=4)
|
|
20
|
+
|
|
21
|
+
def controls(self) -> list[ft.Control]:
|
|
22
|
+
return [self._header, self._event, self._flow, self._body]
|
|
23
|
+
|
|
24
|
+
def show_event(self, event: dict) -> None:
|
|
25
|
+
self._event.value = (
|
|
26
|
+
f"{_fmt(event['timestamp'])} {event['type']} {event['summary']}"
|
|
27
|
+
)
|
|
28
|
+
hint = "select a flow to see its packets" if not event.get("flow_id") else (
|
|
29
|
+
f"flow #{event['flow_id']} - select it in the list for packets"
|
|
30
|
+
)
|
|
31
|
+
self._flow.value = hint
|
|
32
|
+
|
|
33
|
+
def show_flow(self, flow: dict) -> None:
|
|
34
|
+
src = f"{flow['source']}:{flow['src_port']}" if flow["src_port"] else flow["source"]
|
|
35
|
+
dst = (
|
|
36
|
+
f"{flow['destination']}:{flow['dst_port']}"
|
|
37
|
+
if flow["dst_port"]
|
|
38
|
+
else flow["destination"]
|
|
39
|
+
)
|
|
40
|
+
lines = [
|
|
41
|
+
f"Flow #{flow['id']}",
|
|
42
|
+
f"{flow['protocol']} {src} -> {dst}",
|
|
43
|
+
f"state {flow['state']} start {_fmt(flow['start_ts'])} "
|
|
44
|
+
f"end {_fmt(flow['end_ts'])}",
|
|
45
|
+
f"packets {flow['packet_count']} bytes {flow['bytes']}",
|
|
46
|
+
"",
|
|
47
|
+
"packets:",
|
|
48
|
+
]
|
|
49
|
+
self._flow.value = "\n".join(lines)
|
|
50
|
+
tiles: list[ft.Control] = []
|
|
51
|
+
for pkt in flow.get("packets", []):
|
|
52
|
+
src = f"{pkt['source']}:{pkt['src_port']}" if pkt["src_port"] else pkt["source"]
|
|
53
|
+
dst = (
|
|
54
|
+
f"{pkt['destination']}:{pkt['dst_port']}"
|
|
55
|
+
if pkt["dst_port"]
|
|
56
|
+
else pkt["destination"]
|
|
57
|
+
)
|
|
58
|
+
tiles.append(
|
|
59
|
+
ft.Text(
|
|
60
|
+
f"{_fmt(pkt['ts'])} #{pkt['id']} {pkt['protocol']} "
|
|
61
|
+
f"{src} -> {dst} len={pkt['length']}",
|
|
62
|
+
size=11,
|
|
63
|
+
font_family="monospace",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
self._body.controls = tiles
|
|
67
|
+
|
|
68
|
+
def show_message(self, message: str) -> None:
|
|
69
|
+
self._event.value = message
|
|
70
|
+
self._flow.value = ""
|
|
71
|
+
self._body.controls = []
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Flow list widget."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import flet as ft
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FlowList:
|
|
8
|
+
def __init__(self, on_select):
|
|
9
|
+
self._on_select = on_select
|
|
10
|
+
self._count = ft.Text("0 flows", size=11, color=ft.Colors.GREY_400)
|
|
11
|
+
self._list = ft.ListView(expand=True, spacing=2, padding=4, auto_scroll=False)
|
|
12
|
+
|
|
13
|
+
def controls(self) -> list[ft.Control]:
|
|
14
|
+
return [self._count, self._list]
|
|
15
|
+
|
|
16
|
+
def render(self, flows: list[dict]) -> None:
|
|
17
|
+
self._count.value = f"{len(flows)} flows"
|
|
18
|
+
tiles: list[ft.Control] = []
|
|
19
|
+
for flow in flows:
|
|
20
|
+
src = f"{flow['source']}:{flow['src_port']}" if flow["src_port"] else flow["source"]
|
|
21
|
+
dst = (
|
|
22
|
+
f"{flow['destination']}:{flow['dst_port']}"
|
|
23
|
+
if flow["dst_port"]
|
|
24
|
+
else flow["destination"]
|
|
25
|
+
)
|
|
26
|
+
pkts = flow["packet_count"]
|
|
27
|
+
size = flow["bytes"]
|
|
28
|
+
title = ft.Text(f"{src} -> {dst}", size=12, font_family="monospace")
|
|
29
|
+
subtitle = ft.Text(
|
|
30
|
+
f"{flow['protocol']} state={flow['state']} pkts={pkts} bytes={size}",
|
|
31
|
+
size=11,
|
|
32
|
+
color=ft.Colors.GREY_400,
|
|
33
|
+
)
|
|
34
|
+
flow_id = flow["id"]
|
|
35
|
+
tiles.append(
|
|
36
|
+
ft.ListTile(
|
|
37
|
+
title=title,
|
|
38
|
+
subtitle=subtitle,
|
|
39
|
+
dense=True,
|
|
40
|
+
on_click=lambda _e, fid=flow_id: self._on_select(fid),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
tiles.append(ft.Divider(height=1, color=ft.Colors.GREY_800))
|
|
44
|
+
self._list.controls = tiles
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Header widget: capture controls, interface picker, session picker."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import flet as ft
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Header:
|
|
8
|
+
def __init__(self, on_start, on_stop, on_open, on_refresh):
|
|
9
|
+
self._on_start = on_start
|
|
10
|
+
self._on_stop = on_stop
|
|
11
|
+
self._on_open = on_open
|
|
12
|
+
self._on_refresh = on_refresh
|
|
13
|
+
|
|
14
|
+
self.interface = ft.Dropdown(
|
|
15
|
+
width=420,
|
|
16
|
+
label="Interface",
|
|
17
|
+
options=[],
|
|
18
|
+
)
|
|
19
|
+
self.start_btn = ft.FilledButton("Start Capture", on_click=lambda _e: self._on_start())
|
|
20
|
+
self.stop_btn = ft.OutlinedButton("Stop", disabled=True, on_click=lambda _e: self._on_stop())
|
|
21
|
+
self.session = ft.Dropdown(width=220, label="Capture", options=[])
|
|
22
|
+
self.open_btn = ft.OutlinedButton("Open", on_click=lambda _e: self._on_open())
|
|
23
|
+
self.refresh_btn = ft.IconButton(ft.Icons.REFRESH, on_click=lambda _e: self._on_refresh())
|
|
24
|
+
self.status = ft.Text("", size=11, color=ft.Colors.GREY_400)
|
|
25
|
+
self.info = ft.Text("", size=11, color=ft.Colors.TEAL_200)
|
|
26
|
+
self.diag = ft.Text("", size=11, color=ft.Colors.GREY_400)
|
|
27
|
+
|
|
28
|
+
def controls(self) -> list[ft.Control]:
|
|
29
|
+
return [
|
|
30
|
+
ft.Row(
|
|
31
|
+
[
|
|
32
|
+
self.interface,
|
|
33
|
+
self.start_btn,
|
|
34
|
+
self.stop_btn,
|
|
35
|
+
ft.VerticalDivider(),
|
|
36
|
+
self.session,
|
|
37
|
+
self.open_btn,
|
|
38
|
+
self.refresh_btn,
|
|
39
|
+
],
|
|
40
|
+
wrap=True,
|
|
41
|
+
),
|
|
42
|
+
ft.Row([self.info, self.status, self.diag], wrap=True),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def set_interfaces(self, interfaces: list[dict], selected: str | None = None) -> None:
|
|
46
|
+
opts = [
|
|
47
|
+
ft.DropdownOption(
|
|
48
|
+
key=i["name"],
|
|
49
|
+
text=f"{i['name']} [{i['description']}]" if i.get("description") else i["name"],
|
|
50
|
+
tooltip=i.get("description") or i["name"],
|
|
51
|
+
)
|
|
52
|
+
for i in interfaces
|
|
53
|
+
]
|
|
54
|
+
current = self.interface.value
|
|
55
|
+
self.interface.options = opts
|
|
56
|
+
keep = selected or current
|
|
57
|
+
if keep and any(o.key == keep for o in opts):
|
|
58
|
+
self.interface.value = keep
|
|
59
|
+
|
|
60
|
+
def set_sessions(self, sessions: list[dict]) -> None:
|
|
61
|
+
opts = [
|
|
62
|
+
ft.DropdownOption(key=s["session_id"], text=f"{s['name']} ({s['packet_count']} pkts)")
|
|
63
|
+
for s in sessions
|
|
64
|
+
]
|
|
65
|
+
current = self.session.value
|
|
66
|
+
self.session.options = opts
|
|
67
|
+
if current and any(o.key == current for o in opts):
|
|
68
|
+
self.session.value = current
|
|
69
|
+
|
|
70
|
+
def set_capture(self, running: bool, packets: int, flows: int, error: str | None) -> None:
|
|
71
|
+
self.start_btn.disabled = running
|
|
72
|
+
self.stop_btn.disabled = not running
|
|
73
|
+
if error:
|
|
74
|
+
self.status.value = f"capture error: {error}"
|
|
75
|
+
self.status.color = ft.Colors.RED_300
|
|
76
|
+
elif running:
|
|
77
|
+
self.status.value = f"capturing... packets={packets} flows={flows}"
|
|
78
|
+
self.status.color = ft.Colors.AMBER_200
|
|
79
|
+
else:
|
|
80
|
+
self.status.value = f"idle (packets={packets} flows={flows})"
|
|
81
|
+
self.status.color = ft.Colors.GREY_400
|