netflower 1.0.0__py3-none-any.whl
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.
- netflower/__init__.py +5 -0
- netflower/_capture.py +222 -0
- netflower/_constants.py +18 -0
- netflower/_convert.py +248 -0
- netflower/_flow.py +458 -0
- netflower/_libpcap.py +101 -0
- netflower/_parser.py +55 -0
- netflower/_session.py +83 -0
- netflower/_stats.py +50 -0
- netflower/_writer.py +47 -0
- netflower-1.0.0.dist-info/METADATA +178 -0
- netflower-1.0.0.dist-info/RECORD +15 -0
- netflower-1.0.0.dist-info/WHEEL +5 -0
- netflower-1.0.0.dist-info/licenses/LICENSE +21 -0
- netflower-1.0.0.dist-info/top_level.txt +1 -0
netflower/__init__.py
ADDED
netflower/_capture.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live packet capture using system libpcap via ctypes.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from netflower import capture_live
|
|
6
|
+
|
|
7
|
+
handle = capture_live("eth0", on_flow=lambda flow: print(flow), idle_timeout=30)
|
|
8
|
+
handle.start()
|
|
9
|
+
# ... do work ...
|
|
10
|
+
handle.stop()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ctypes
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
from ._libpcap import (
|
|
17
|
+
PcapHandler_cb, PktHdr,
|
|
18
|
+
pcap_open_live, pcap_loop, pcap_breakloop, pcap_close,
|
|
19
|
+
pcap_dump_open, pcap_dump, pcap_dump_close,
|
|
20
|
+
)
|
|
21
|
+
from ._parser import _parse_packet
|
|
22
|
+
from ._session import FlowSession
|
|
23
|
+
from ._constants import FLOW_TIMEOUT
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CallbackWriter:
|
|
27
|
+
"""Writer that calls on_flow(dict) instead of writing to a CSV file."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, on_flow: callable) -> None:
|
|
30
|
+
self._on_flow = on_flow
|
|
31
|
+
|
|
32
|
+
def write(self, flow_dict: dict) -> None:
|
|
33
|
+
self._on_flow(flow_dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _PcapSavingWriter:
|
|
37
|
+
"""
|
|
38
|
+
Wraps on_flow callable. When a flow is emitted, also dumps its raw packets
|
|
39
|
+
to a .pcap file in pcap_dir before clearing the buffer.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, on_flow: callable, pcap_dir: str, pcap_handle) -> None:
|
|
43
|
+
self._on_flow = on_flow
|
|
44
|
+
self._pcap_dir = pcap_dir
|
|
45
|
+
self._pcap_handle = pcap_handle
|
|
46
|
+
# flow_key -> list of (PktHdr, bytes)
|
|
47
|
+
self._buffers: dict[tuple, list] = {}
|
|
48
|
+
|
|
49
|
+
def write(self, flow_dict: dict) -> None:
|
|
50
|
+
key = (
|
|
51
|
+
flow_dict.get("protocol"),
|
|
52
|
+
flow_dict.get("src_ip"),
|
|
53
|
+
flow_dict.get("dst_ip"),
|
|
54
|
+
flow_dict.get("src_port"),
|
|
55
|
+
flow_dict.get("dst_port"),
|
|
56
|
+
)
|
|
57
|
+
packets = self._buffers.pop(key, [])
|
|
58
|
+
if packets:
|
|
59
|
+
ts_us = int(flow_dict.get("timestamp", 0) * 1_000_000)
|
|
60
|
+
fname = (
|
|
61
|
+
f"{self._pcap_dir}/"
|
|
62
|
+
f"{flow_dict.get('src_ip')}-{flow_dict.get('src_port')}-"
|
|
63
|
+
f"{flow_dict.get('dst_ip')}-{flow_dict.get('dst_port')}-"
|
|
64
|
+
f"{flow_dict.get('protocol')}-{ts_us}.pcap"
|
|
65
|
+
).encode()
|
|
66
|
+
dumper = pcap_dump_open(self._pcap_handle, fname)
|
|
67
|
+
if dumper:
|
|
68
|
+
for hdr, data in packets:
|
|
69
|
+
pcap_dump(dumper, ctypes.byref(hdr), data)
|
|
70
|
+
pcap_dump_close(dumper)
|
|
71
|
+
self._on_flow(flow_dict)
|
|
72
|
+
|
|
73
|
+
def buffer_packet(self, key: tuple, hdr: PktHdr, data: bytes) -> None:
|
|
74
|
+
if key not in self._buffers:
|
|
75
|
+
self._buffers[key] = []
|
|
76
|
+
self._buffers[key].append((hdr, data))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CaptureHandle:
|
|
80
|
+
"""
|
|
81
|
+
Handle returned by capture_live(). Controls start/stop of capture thread.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
interface: Network interface name as bytes (e.g. b"eth0").
|
|
86
|
+
on_flow: Callable receiving a flow dict when a flow completes.
|
|
87
|
+
idle_timeout: Seconds of inactivity before a flow is emitted.
|
|
88
|
+
flow_timeout: Absolute max flow duration before forced emit.
|
|
89
|
+
save_pcap: If True, save raw packets of each completed flow to pcap_dir.
|
|
90
|
+
pcap_dir: Directory path for .pcap files. Required when save_pcap=True.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
_GC_INTERVAL = 1000
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
interface: bytes,
|
|
98
|
+
on_flow: callable,
|
|
99
|
+
idle_timeout: float,
|
|
100
|
+
flow_timeout: float,
|
|
101
|
+
save_pcap: bool,
|
|
102
|
+
pcap_dir: str | None,
|
|
103
|
+
) -> None:
|
|
104
|
+
if save_pcap and pcap_dir is None:
|
|
105
|
+
raise ValueError("pcap_dir is required when save_pcap=True")
|
|
106
|
+
self._interface = interface
|
|
107
|
+
self._on_flow = on_flow
|
|
108
|
+
self._idle_timeout = idle_timeout
|
|
109
|
+
self._flow_timeout = flow_timeout
|
|
110
|
+
self._save_pcap = save_pcap
|
|
111
|
+
self._pcap_dir = pcap_dir
|
|
112
|
+
self._handle = None
|
|
113
|
+
self._thread: threading.Thread | None = None
|
|
114
|
+
self._session = None
|
|
115
|
+
self._writer = None
|
|
116
|
+
self._callback = None
|
|
117
|
+
|
|
118
|
+
def start(self) -> None:
|
|
119
|
+
errbuf = ctypes.create_string_buffer(256)
|
|
120
|
+
self._handle = pcap_open_live(self._interface, 65535, 1, 1000, errbuf)
|
|
121
|
+
if not self._handle:
|
|
122
|
+
raise RuntimeError(f"pcap_open_live failed: {errbuf.value.decode()}")
|
|
123
|
+
|
|
124
|
+
if self._save_pcap:
|
|
125
|
+
self._writer = _PcapSavingWriter(self._on_flow, self._pcap_dir, self._handle)
|
|
126
|
+
else:
|
|
127
|
+
self._writer = CallbackWriter(self._on_flow)
|
|
128
|
+
|
|
129
|
+
self._session = FlowSession(self._writer, self._flow_timeout)
|
|
130
|
+
self._pkt_counter = 0
|
|
131
|
+
|
|
132
|
+
@PcapHandler_cb
|
|
133
|
+
def _callback(user, pkthdr_p, pkt_data):
|
|
134
|
+
hdr = pkthdr_p.contents
|
|
135
|
+
ts = hdr.ts_sec + hdr.ts_usec / 1_000_000
|
|
136
|
+
buf = ctypes.string_at(pkt_data, hdr.caplen)
|
|
137
|
+
parsed = _parse_packet(buf)
|
|
138
|
+
if parsed is None:
|
|
139
|
+
return
|
|
140
|
+
src_ip, dst_ip, src_port, dst_port, proto, pkt_len, hdr_len, pay_len, flags, win = parsed
|
|
141
|
+
if self._save_pcap:
|
|
142
|
+
fwd_key = (proto, src_ip, dst_ip, src_port, dst_port)
|
|
143
|
+
bwd_key = (proto, dst_ip, src_ip, dst_port, src_port)
|
|
144
|
+
key = fwd_key if (fwd_key in self._session._flows
|
|
145
|
+
or bwd_key not in self._session._flows) else bwd_key
|
|
146
|
+
self._writer.buffer_packet(key, hdr, buf)
|
|
147
|
+
self._session.process(ts, src_ip, dst_ip, src_port, dst_port,
|
|
148
|
+
proto, pkt_len, hdr_len, pay_len, flags, win)
|
|
149
|
+
self._pkt_counter += 1
|
|
150
|
+
if self._pkt_counter % self._GC_INTERVAL == 0:
|
|
151
|
+
self._session.gc(ts)
|
|
152
|
+
|
|
153
|
+
# Keep reference to prevent ctypes GC
|
|
154
|
+
self._callback = _callback
|
|
155
|
+
|
|
156
|
+
self._thread = threading.Thread(
|
|
157
|
+
target=pcap_loop,
|
|
158
|
+
args=(self._handle, -1, self._callback, None),
|
|
159
|
+
daemon=True,
|
|
160
|
+
)
|
|
161
|
+
self._thread.start()
|
|
162
|
+
|
|
163
|
+
def stop(self) -> None:
|
|
164
|
+
if self._handle:
|
|
165
|
+
pcap_breakloop(self._handle)
|
|
166
|
+
if self._thread:
|
|
167
|
+
self._thread.join(timeout=5)
|
|
168
|
+
if self._session:
|
|
169
|
+
self._session.flush_all()
|
|
170
|
+
if self._handle:
|
|
171
|
+
pcap_close(self._handle)
|
|
172
|
+
self._handle = None
|
|
173
|
+
|
|
174
|
+
def __enter__(self):
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def __exit__(self, *_):
|
|
178
|
+
self.stop()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def capture_live(
|
|
182
|
+
interface: str,
|
|
183
|
+
on_flow: callable,
|
|
184
|
+
idle_timeout: float = 30.0,
|
|
185
|
+
flow_timeout: float = FLOW_TIMEOUT,
|
|
186
|
+
save_pcap: bool = False,
|
|
187
|
+
pcap_dir: str | None = None,
|
|
188
|
+
) -> CaptureHandle:
|
|
189
|
+
"""
|
|
190
|
+
Capture live packets from a network interface and emit completed flows.
|
|
191
|
+
|
|
192
|
+
Requires root or CAP_NET_RAW privilege.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
interface: Network interface name (e.g. "eth0").
|
|
197
|
+
on_flow: Called with a flow dict (82 fields) when a flow completes.
|
|
198
|
+
Same column names as convert_pcap_to_csv output.
|
|
199
|
+
idle_timeout: Seconds of inactivity before a flow is emitted (default 30).
|
|
200
|
+
flow_timeout: Absolute max flow duration in seconds (default 120).
|
|
201
|
+
save_pcap: Save raw packets of each completed flow to a .pcap file.
|
|
202
|
+
pcap_dir: Directory for .pcap files. Required when save_pcap=True.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
CaptureHandle — call .start() to begin capture, .stop() to end.
|
|
207
|
+
|
|
208
|
+
Example
|
|
209
|
+
-------
|
|
210
|
+
>>> handle = capture_live("eth0", on_flow=lambda f: print(f["flow_duration"]))
|
|
211
|
+
>>> handle.start()
|
|
212
|
+
>>> import time; time.sleep(60)
|
|
213
|
+
>>> handle.stop()
|
|
214
|
+
"""
|
|
215
|
+
return CaptureHandle(
|
|
216
|
+
interface=interface.encode() if isinstance(interface, str) else interface,
|
|
217
|
+
on_flow=on_flow,
|
|
218
|
+
idle_timeout=idle_timeout,
|
|
219
|
+
flow_timeout=flow_timeout,
|
|
220
|
+
save_pcap=save_pcap,
|
|
221
|
+
pcap_dir=pcap_dir,
|
|
222
|
+
)
|
netflower/_constants.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FLOW_TIMEOUT = 120.0 # seconds of inactivity before a flow is flushed
|
|
2
|
+
ACTIVE_TIMEOUT = 5.0 # gap > this separates active from idle periods
|
|
3
|
+
CLUMP_TIMEOUT = 1.0 # gap > this starts a new subflow
|
|
4
|
+
BULK_BOUND = 4 # minimum packets required to register a bulk transfer
|
|
5
|
+
CSV_BUFFER_ROWS = 500 # rows buffered in memory before writing to disk
|
|
6
|
+
|
|
7
|
+
# TCP flag bitmasks (RFC 793 / RFC 3168)
|
|
8
|
+
TCP_FIN = 0x01
|
|
9
|
+
TCP_SYN = 0x02
|
|
10
|
+
TCP_RST = 0x04
|
|
11
|
+
TCP_PSH = 0x08
|
|
12
|
+
TCP_ACK = 0x10
|
|
13
|
+
TCP_URG = 0x20
|
|
14
|
+
TCP_ECE = 0x40
|
|
15
|
+
TCP_CWR = 0x80
|
|
16
|
+
|
|
17
|
+
FORWARD = 0
|
|
18
|
+
BACKWARD = 1
|
netflower/_convert.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main conversion pipeline: PCAP → FlowSession → CSV.
|
|
3
|
+
|
|
4
|
+
Supports both pcap and pcapng formats.
|
|
5
|
+
Packets are streamed one by one; no packet objects are retained after processing.
|
|
6
|
+
|
|
7
|
+
Parallelism strategy (n_jobs > 1):
|
|
8
|
+
The main process reads packets sequentially and routes each one to a worker
|
|
9
|
+
subprocess based on a deterministic hash of the bidirectional flow key.
|
|
10
|
+
Each worker owns a disjoint subset of flows and writes to a temp CSV.
|
|
11
|
+
Temp CSVs are merged into the final output after all workers finish.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import tempfile
|
|
16
|
+
from multiprocessing import Process, Queue
|
|
17
|
+
from typing import Iterator
|
|
18
|
+
|
|
19
|
+
import dpkt
|
|
20
|
+
import dpkt.pcapng
|
|
21
|
+
|
|
22
|
+
from ._session import FlowSession
|
|
23
|
+
from ._writer import CsvWriter
|
|
24
|
+
from ._parser import _parse_packet
|
|
25
|
+
from ._constants import FLOW_TIMEOUT, CSV_BUFFER_ROWS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _open_pcap(path: str):
|
|
29
|
+
"""Return an iterable of (timestamp, raw_bytes) from a pcap or pcapng file."""
|
|
30
|
+
with open(path, "rb") as fh:
|
|
31
|
+
magic = fh.read(4)
|
|
32
|
+
|
|
33
|
+
if magic == b"\x0a\x0d\x0d\x0a":
|
|
34
|
+
return _iter_pcapng(path)
|
|
35
|
+
return _iter_pcap(path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _iter_pcap(path: str) -> Iterator[tuple[float, bytes]]:
|
|
39
|
+
with open(path, "rb") as fh:
|
|
40
|
+
yield from dpkt.pcap.Reader(fh)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _iter_pcapng(path: str) -> Iterator[tuple[float, bytes]]:
|
|
44
|
+
with open(path, "rb") as fh:
|
|
45
|
+
yield from dpkt.pcapng.Reader(fh)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _route(src_ip: str, dst_ip: str, src_port: int, dst_port: int, proto: int, n_jobs: int) -> int:
|
|
49
|
+
"""Return the worker index for a bidirectional flow (order-independent)."""
|
|
50
|
+
a = (src_ip, src_port)
|
|
51
|
+
b = (dst_ip, dst_port)
|
|
52
|
+
if a > b:
|
|
53
|
+
a, b = b, a
|
|
54
|
+
return hash((a, b, proto)) % n_jobs
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _worker(task_queue: Queue, result_queue: Queue, output_path: str,
|
|
58
|
+
flow_timeout: float, buffer_rows: int) -> None:
|
|
59
|
+
"""Subprocess: drain task_queue, track flows, write CSV to output_path."""
|
|
60
|
+
with CsvWriter(output_path, buffer_rows) as writer:
|
|
61
|
+
session = FlowSession(writer, flow_timeout)
|
|
62
|
+
while True:
|
|
63
|
+
item = task_queue.get()
|
|
64
|
+
if item is None:
|
|
65
|
+
break
|
|
66
|
+
kind = item[0]
|
|
67
|
+
if kind == "pkt":
|
|
68
|
+
_, ts, src_ip, dst_ip, src_port, dst_port, proto, pkt_len, hdr_len, pay_len, flags, win = item
|
|
69
|
+
session.process(ts, src_ip, dst_ip, src_port, dst_port, proto,
|
|
70
|
+
pkt_len, hdr_len, pay_len, flags, win)
|
|
71
|
+
elif kind == "gc":
|
|
72
|
+
session.gc(item[1])
|
|
73
|
+
session.flush_all()
|
|
74
|
+
result_queue.put(writer.row_count)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _merge_csvs(sources: list[str], output_path: str) -> None:
|
|
78
|
+
import csv
|
|
79
|
+
|
|
80
|
+
header_written = False
|
|
81
|
+
fieldnames = None
|
|
82
|
+
with open(output_path, "w", newline="", buffering=1 << 16) as out_fh:
|
|
83
|
+
out_writer = None
|
|
84
|
+
for src in sources:
|
|
85
|
+
if not os.path.exists(src) or os.path.getsize(src) == 0:
|
|
86
|
+
continue
|
|
87
|
+
with open(src, "r", newline="") as in_fh:
|
|
88
|
+
reader = csv.DictReader(in_fh)
|
|
89
|
+
if not header_written:
|
|
90
|
+
fieldnames = reader.fieldnames
|
|
91
|
+
out_writer = csv.DictWriter(out_fh, fieldnames=fieldnames)
|
|
92
|
+
out_writer.writeheader()
|
|
93
|
+
header_written = True
|
|
94
|
+
for row in reader:
|
|
95
|
+
out_writer.writerow(row)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def convert_pcap_to_csv(
|
|
99
|
+
input_path: str,
|
|
100
|
+
output_path: str,
|
|
101
|
+
flow_timeout: float = FLOW_TIMEOUT,
|
|
102
|
+
gc_interval: int = 1000,
|
|
103
|
+
buffer_rows: int = CSV_BUFFER_ROWS,
|
|
104
|
+
n_jobs: int = 1,
|
|
105
|
+
) -> int:
|
|
106
|
+
"""
|
|
107
|
+
Convert a PCAP (or pcapng) file into a flow-based CSV file.
|
|
108
|
+
|
|
109
|
+
Each row in the output CSV represents one bidirectional network flow and
|
|
110
|
+
contains 82 features compatible with the CICFlowMeter feature set.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
input_path: Path to the input .pcap or .pcapng file.
|
|
115
|
+
output_path: Path for the output .csv file (created or overwritten).
|
|
116
|
+
flow_timeout: Seconds of inactivity before a flow is flushed (default 120).
|
|
117
|
+
gc_interval: Run idle-flow garbage collection every N packets (default 1000).
|
|
118
|
+
buffer_rows: Rows buffered in memory before a disk write (default 500).
|
|
119
|
+
n_jobs: Number of worker processes (default 1).
|
|
120
|
+
-1 uses all available CPUs.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
Number of flow rows written to the CSV.
|
|
125
|
+
"""
|
|
126
|
+
if n_jobs == 0:
|
|
127
|
+
raise ValueError("n_jobs=0 is invalid. Use n_jobs=1 for single-process or n_jobs=-1 for all CPUs.")
|
|
128
|
+
if n_jobs == -1:
|
|
129
|
+
n_jobs = os.cpu_count() or 1
|
|
130
|
+
elif n_jobs < -1:
|
|
131
|
+
n_jobs = max(1, (os.cpu_count() or 1) + n_jobs + 1)
|
|
132
|
+
|
|
133
|
+
if n_jobs == 1:
|
|
134
|
+
return _convert_single(input_path, output_path, flow_timeout, gc_interval, buffer_rows)
|
|
135
|
+
return _convert_parallel(input_path, output_path, flow_timeout, gc_interval, buffer_rows, n_jobs)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _convert_single(
|
|
139
|
+
input_path: str,
|
|
140
|
+
output_path: str,
|
|
141
|
+
flow_timeout: float,
|
|
142
|
+
gc_interval: int,
|
|
143
|
+
buffer_rows: int,
|
|
144
|
+
) -> int:
|
|
145
|
+
with CsvWriter(output_path, buffer_rows) as writer:
|
|
146
|
+
session = FlowSession(writer, flow_timeout)
|
|
147
|
+
pkt_counter = 0
|
|
148
|
+
last_ts = 0.0
|
|
149
|
+
|
|
150
|
+
for ts, buf in _open_pcap(input_path):
|
|
151
|
+
parsed = _parse_packet(buf)
|
|
152
|
+
if parsed is None:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
src_ip, dst_ip, src_port, dst_port, proto, pkt_len, hdr_len, pay_len, flags, win = parsed
|
|
156
|
+
session.process(ts, src_ip, dst_ip, src_port, dst_port, proto,
|
|
157
|
+
pkt_len, hdr_len, pay_len, flags, win)
|
|
158
|
+
|
|
159
|
+
pkt_counter += 1
|
|
160
|
+
last_ts = ts
|
|
161
|
+
|
|
162
|
+
if pkt_counter % gc_interval == 0:
|
|
163
|
+
session.gc(last_ts)
|
|
164
|
+
|
|
165
|
+
session.flush_all()
|
|
166
|
+
|
|
167
|
+
return writer.row_count
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _convert_parallel(
|
|
171
|
+
input_path: str,
|
|
172
|
+
output_path: str,
|
|
173
|
+
flow_timeout: float,
|
|
174
|
+
gc_interval: int,
|
|
175
|
+
buffer_rows: int,
|
|
176
|
+
n_jobs: int,
|
|
177
|
+
) -> int:
|
|
178
|
+
tmp_dir = tempfile.mkdtemp(prefix="netflower_")
|
|
179
|
+
tmp_files = [os.path.join(tmp_dir, f"worker_{i}.csv") for i in range(n_jobs)]
|
|
180
|
+
task_queues = [Queue(maxsize=2000) for _ in range(n_jobs)]
|
|
181
|
+
result_queue: Queue = Queue()
|
|
182
|
+
|
|
183
|
+
workers = [
|
|
184
|
+
Process(
|
|
185
|
+
target=_worker,
|
|
186
|
+
args=(task_queues[i], result_queue, tmp_files[i], flow_timeout, buffer_rows),
|
|
187
|
+
daemon=True,
|
|
188
|
+
)
|
|
189
|
+
for i in range(n_jobs)
|
|
190
|
+
]
|
|
191
|
+
for w in workers:
|
|
192
|
+
w.start()
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
pkt_counter = 0
|
|
196
|
+
last_ts = 0.0
|
|
197
|
+
|
|
198
|
+
for ts, buf in _open_pcap(input_path):
|
|
199
|
+
parsed = _parse_packet(buf)
|
|
200
|
+
if parsed is None:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
src_ip, dst_ip, src_port, dst_port, proto, pkt_len, hdr_len, pay_len, flags, win = parsed
|
|
204
|
+
wid = _route(src_ip, dst_ip, src_port, dst_port, proto, n_jobs)
|
|
205
|
+
task_queues[wid].put((
|
|
206
|
+
"pkt", ts, src_ip, dst_ip, src_port, dst_port, proto,
|
|
207
|
+
pkt_len, hdr_len, pay_len, flags, win,
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
pkt_counter += 1
|
|
211
|
+
last_ts = ts
|
|
212
|
+
|
|
213
|
+
if pkt_counter % gc_interval == 0:
|
|
214
|
+
for q in task_queues:
|
|
215
|
+
q.put(("gc", last_ts))
|
|
216
|
+
|
|
217
|
+
for q in task_queues:
|
|
218
|
+
q.put(None)
|
|
219
|
+
|
|
220
|
+
for w in workers:
|
|
221
|
+
w.join()
|
|
222
|
+
if w.exitcode != 0:
|
|
223
|
+
raise RuntimeError(f"Worker process exited with code {w.exitcode}")
|
|
224
|
+
|
|
225
|
+
except Exception:
|
|
226
|
+
for q in task_queues:
|
|
227
|
+
try:
|
|
228
|
+
q.put_nowait(None)
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
for w in workers:
|
|
232
|
+
w.terminate()
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
total = sum(result_queue.get() for _ in range(n_jobs))
|
|
236
|
+
_merge_csvs(tmp_files, output_path)
|
|
237
|
+
|
|
238
|
+
for f in tmp_files:
|
|
239
|
+
try:
|
|
240
|
+
os.unlink(f)
|
|
241
|
+
except OSError:
|
|
242
|
+
pass
|
|
243
|
+
try:
|
|
244
|
+
os.rmdir(tmp_dir)
|
|
245
|
+
except OSError:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
return total
|