sltmodel 0.1.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.
- sltmodel/__init__.py +43 -0
- sltmodel/infosize.py +93 -0
- sltmodel/packet_data.py +518 -0
- sltmodel/payload_struct_defs.py +129 -0
- sltmodel/resources/__init__.py +22 -0
- sltmodel/resources/pcap.json +604 -0
- sltmodel/resources/pcapng.json +535 -0
- sltmodel/struct_instance.py +185 -0
- sltmodel-0.1.0.dist-info/METADATA +52 -0
- sltmodel-0.1.0.dist-info/RECORD +12 -0
- sltmodel-0.1.0.dist-info/WHEEL +4 -0
- sltmodel-0.1.0.dist-info/licenses/LICENSE +21 -0
sltmodel/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""GUI-independent data models and operations for StructLayoutToolkit."""
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
|
|
4
|
+
_EXPORTS = {
|
|
5
|
+
"format_infosize": (".infosize", "format_infosize"),
|
|
6
|
+
"infosize_to_json_data": (".infosize", "infosize_to_json_data"),
|
|
7
|
+
"infosize_value_to_string": (".infosize", "infosize_value_to_string"),
|
|
8
|
+
"parse_infosize_input": (".infosize", "parse_infosize_input"),
|
|
9
|
+
"CaptureDocument": (".packet_data", "CaptureDocument"),
|
|
10
|
+
"PacketFragment": (".packet_data", "PacketFragment"),
|
|
11
|
+
"ReassembledPacket": (".packet_data", "ReassembledPacket"),
|
|
12
|
+
"internet_checksum": (".packet_data", "internet_checksum"),
|
|
13
|
+
"PayloadStructDef": (".payload_struct_defs", "PayloadStructDef"),
|
|
14
|
+
"load_payload_struct_defs": (".payload_struct_defs",
|
|
15
|
+
"load_payload_struct_defs"),
|
|
16
|
+
"matching_struct_layout": (".payload_struct_defs",
|
|
17
|
+
"matching_struct_layout"),
|
|
18
|
+
"packet_eval_env": (".payload_struct_defs", "packet_eval_env"),
|
|
19
|
+
"save_payload_struct_defs": (".payload_struct_defs",
|
|
20
|
+
"save_payload_struct_defs"),
|
|
21
|
+
"field_instance_at_path": (".struct_instance", "field_instance_at_path"),
|
|
22
|
+
"format_field_value": (".struct_instance", "format_field_value"),
|
|
23
|
+
"format_type": (".struct_instance", "format_type"),
|
|
24
|
+
"format_value": (".struct_instance", "format_value"),
|
|
25
|
+
"minimum_struct_size": (".struct_instance", "minimum_struct_size"),
|
|
26
|
+
"parse_field_value": (".struct_instance", "parse_field_value"),
|
|
27
|
+
"parse_value": (".struct_instance", "parse_value"),
|
|
28
|
+
"replace_instance_field_value": (".struct_instance",
|
|
29
|
+
"replace_instance_field_value"),
|
|
30
|
+
"replace_instance_value": (".struct_instance", "replace_instance_value"),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
__all__ = list(_EXPORTS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def __getattr__(name: str):
|
|
37
|
+
"""Load model APIs only when requested."""
|
|
38
|
+
try:
|
|
39
|
+
module_name, attribute_name = _EXPORTS[name]
|
|
40
|
+
except KeyError as exc:
|
|
41
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}"
|
|
42
|
+
) from exc
|
|
43
|
+
return getattr(import_module(module_name, __name__), attribute_name)
|
sltmodel/infosize.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Shared InfoSize parsing/formatting helpers used by the editor windows.
|
|
2
|
+
|
|
3
|
+
These take the caller's InfoSize class as a parameter instead of importing
|
|
4
|
+
one directly, so callers (and their tests) can supply their own InfoSize
|
|
5
|
+
implementation without this module caching a stale class reference.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_infosize(value: object) -> str:
|
|
13
|
+
"""Format an InfoSize-like value as its ``byte,bit`` string form."""
|
|
14
|
+
return f"{value.byte},{value.bit}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_infosize_input(value: object, info_size_cls: type) -> object:
|
|
18
|
+
"""Parse ``byte,bit`` input, preserving non-static expressions."""
|
|
19
|
+
if isinstance(value, info_size_cls):
|
|
20
|
+
return value
|
|
21
|
+
if isinstance(value, dict) and value.get("__type__") == "InfoSize":
|
|
22
|
+
return info_size_cls(int(value.get("byte", 0)), int(value.get("bit",
|
|
23
|
+
0)))
|
|
24
|
+
text = str(value).strip()
|
|
25
|
+
if not text:
|
|
26
|
+
text = "0,0"
|
|
27
|
+
try:
|
|
28
|
+
parsed = json.loads(text)
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
parsed = None
|
|
31
|
+
if isinstance(parsed, dict) and parsed.get("__type__") == "InfoSize":
|
|
32
|
+
return info_size_cls(int(parsed.get("byte", 0)),
|
|
33
|
+
int(parsed.get("bit", 0)))
|
|
34
|
+
parts = [part.strip() for part in text.split(",")]
|
|
35
|
+
if len(parts) == 1:
|
|
36
|
+
try:
|
|
37
|
+
return info_size_cls(int(parts[0]), 0)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return text
|
|
40
|
+
if len(parts) == 2:
|
|
41
|
+
try:
|
|
42
|
+
byte, bit = (int(part) for part in parts)
|
|
43
|
+
except ValueError:
|
|
44
|
+
return text
|
|
45
|
+
return info_size_cls(byte, bit)
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def infosize_value_to_string(value: object, info_size_cls: type) -> str:
|
|
50
|
+
"""Convert an InfoSize-like value to one editable cell string."""
|
|
51
|
+
|
|
52
|
+
def from_dict(item: dict) -> str:
|
|
53
|
+
info_size = info_size_cls(int(item.get("byte", 0)),
|
|
54
|
+
int(item.get("bit", 0)))
|
|
55
|
+
return format_infosize(info_size)
|
|
56
|
+
|
|
57
|
+
def from_string(item: str) -> str:
|
|
58
|
+
stripped = item.strip()
|
|
59
|
+
if not stripped:
|
|
60
|
+
return "0,0"
|
|
61
|
+
try:
|
|
62
|
+
parsed = json.loads(stripped)
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
try:
|
|
65
|
+
info_size = info_size_cls(int(stripped), 0)
|
|
66
|
+
except ValueError:
|
|
67
|
+
return stripped
|
|
68
|
+
return format_infosize(info_size)
|
|
69
|
+
if isinstance(parsed, dict) and parsed.get("__type__") == "InfoSize":
|
|
70
|
+
return from_dict(parsed)
|
|
71
|
+
return stripped
|
|
72
|
+
|
|
73
|
+
if isinstance(value, info_size_cls):
|
|
74
|
+
return format_infosize(value)
|
|
75
|
+
|
|
76
|
+
handlers = {
|
|
77
|
+
dict:
|
|
78
|
+
lambda item: from_dict(item)
|
|
79
|
+
if item.get("__type__") == "InfoSize" else "0,0",
|
|
80
|
+
str:
|
|
81
|
+
from_string,
|
|
82
|
+
int:
|
|
83
|
+
lambda item: from_dict({
|
|
84
|
+
"byte": item,
|
|
85
|
+
"bit": 0
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
return handlers.get(type(value), lambda _: "0,0")(value)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def infosize_to_json_data(value: object) -> dict:
|
|
92
|
+
"""Convert an InfoSize-like instance to its typed JSON dictionary."""
|
|
93
|
+
return json.loads(value.serialize())
|
sltmodel/packet_data.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""Capture packet extraction and IP fragment reassembly."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ipaddress
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from sltcodec import decode
|
|
10
|
+
from sltcore import virtual_bytearray
|
|
11
|
+
|
|
12
|
+
from .resources import load_pcap_layout, load_pcapng_layout
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PacketFragment:
|
|
17
|
+
"""Location of one IP payload fragment in the capture file."""
|
|
18
|
+
|
|
19
|
+
sequence: int
|
|
20
|
+
timestamps: dict[str, object]
|
|
21
|
+
capture_offset: int
|
|
22
|
+
payload_offset: int
|
|
23
|
+
length: int
|
|
24
|
+
more_fragments: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ReassembledPacket:
|
|
29
|
+
"""One complete or partial reassembled IP payload."""
|
|
30
|
+
|
|
31
|
+
sequence: int
|
|
32
|
+
timestamps: dict[str, object]
|
|
33
|
+
key: tuple
|
|
34
|
+
ip_version: int
|
|
35
|
+
source_bytes: bytes
|
|
36
|
+
destination_bytes: bytes
|
|
37
|
+
protocol: int
|
|
38
|
+
identification: int | None
|
|
39
|
+
fragments: list[PacketFragment]
|
|
40
|
+
data: virtual_bytearray
|
|
41
|
+
complete: bool
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def source(self) -> str:
|
|
45
|
+
"""Return the source IP address as a string."""
|
|
46
|
+
return str(ipaddress.ip_address(self.source_bytes))
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def destination(self) -> str:
|
|
50
|
+
"""Return the destination IP address as a string."""
|
|
51
|
+
return str(ipaddress.ip_address(self.destination_bytes))
|
|
52
|
+
|
|
53
|
+
def replace_data(
|
|
54
|
+
self,
|
|
55
|
+
capture_data: bytearray,
|
|
56
|
+
data: bytes | bytearray | virtual_bytearray,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Write same-length reassembled data back to all source fragments."""
|
|
59
|
+
if not self.complete:
|
|
60
|
+
raise ValueError("Incomplete fragmented packets cannot be edited.")
|
|
61
|
+
if len(data) != len(self.data):
|
|
62
|
+
raise ValueError("Encoded data must keep the reassembled length.")
|
|
63
|
+
|
|
64
|
+
del capture_data
|
|
65
|
+
if data is not self.data:
|
|
66
|
+
self.data.write_slice(0, data)
|
|
67
|
+
self._update_transport_checksum(self.data)
|
|
68
|
+
|
|
69
|
+
def _update_transport_checksum(self, data: virtual_bytearray) -> None:
|
|
70
|
+
"""Update the transport layer checksum for the given data."""
|
|
71
|
+
checksum_offset = {6: 16, 17: 6, 1: 2, 58: 2}.get(self.protocol)
|
|
72
|
+
if checksum_offset is None or len(data) < checksum_offset + 2:
|
|
73
|
+
return
|
|
74
|
+
data.write_slice(checksum_offset, b"\x00\x00")
|
|
75
|
+
if self.protocol == 1:
|
|
76
|
+
checksum_data = data
|
|
77
|
+
elif self.ip_version == 4:
|
|
78
|
+
checksum_data = virtual_bytearray((
|
|
79
|
+
self.source_bytes,
|
|
80
|
+
self.destination_bytes,
|
|
81
|
+
bytes((0, self.protocol)),
|
|
82
|
+
len(data).to_bytes(2, "big"),
|
|
83
|
+
data,
|
|
84
|
+
))
|
|
85
|
+
else:
|
|
86
|
+
checksum_data = virtual_bytearray((
|
|
87
|
+
self.source_bytes,
|
|
88
|
+
self.destination_bytes,
|
|
89
|
+
len(data).to_bytes(4, "big"),
|
|
90
|
+
b"\x00\x00\x00",
|
|
91
|
+
bytes((self.protocol, )),
|
|
92
|
+
data,
|
|
93
|
+
))
|
|
94
|
+
checksum = internet_checksum(checksum_data)
|
|
95
|
+
data.write_slice(checksum_offset, checksum.to_bytes(2, "big"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class _RawFragment:
|
|
100
|
+
"""Representation of a raw IP fragment."""
|
|
101
|
+
sequence: int
|
|
102
|
+
timestamps: dict[str, object]
|
|
103
|
+
key: tuple
|
|
104
|
+
ip_version: int
|
|
105
|
+
source: bytes
|
|
106
|
+
destination: bytes
|
|
107
|
+
protocol: int
|
|
108
|
+
identification: int | None
|
|
109
|
+
offset: int
|
|
110
|
+
more_fragments: bool
|
|
111
|
+
capture_offset: int
|
|
112
|
+
data: virtual_bytearray
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_Frame = tuple[int, int, int, dict[str, object], object]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CaptureDocument:
|
|
119
|
+
"""Decoded capture data with reassembled IP payloads."""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
data: bytes | bytearray,
|
|
124
|
+
capture_format: str,
|
|
125
|
+
decode_transform: Callable[[object], object] | None = None,
|
|
126
|
+
*,
|
|
127
|
+
progress_callback: Callable[[float], None] | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
self.data = data if isinstance(data, bytearray) else bytearray(data)
|
|
130
|
+
self.capture_format = capture_format
|
|
131
|
+
layout = (load_pcap_layout()
|
|
132
|
+
if capture_format == "pcap" else load_pcapng_layout())
|
|
133
|
+
report_progress = progress_callback or (lambda _progress: None)
|
|
134
|
+
self.struct_instance = decode(
|
|
135
|
+
layout,
|
|
136
|
+
self.data,
|
|
137
|
+
progress_callback=lambda progress: report_progress(progress * 0.65),
|
|
138
|
+
)
|
|
139
|
+
if decode_transform is not None:
|
|
140
|
+
self.struct_instance = decode_transform(self.struct_instance)
|
|
141
|
+
report_progress(0.65)
|
|
142
|
+
frames = _frames(self.struct_instance, capture_format)
|
|
143
|
+
fragments = []
|
|
144
|
+
for sequence, frame in enumerate(frames):
|
|
145
|
+
offset, length, link_type, timestamps, packet_data = frame
|
|
146
|
+
fragment = _extract_ip_fragment(
|
|
147
|
+
self.data,
|
|
148
|
+
offset,
|
|
149
|
+
length,
|
|
150
|
+
link_type,
|
|
151
|
+
sequence,
|
|
152
|
+
timestamps,
|
|
153
|
+
packet_data,
|
|
154
|
+
)
|
|
155
|
+
if fragment is not None:
|
|
156
|
+
fragments.append(fragment)
|
|
157
|
+
report_progress(0.65 + 0.25 * (sequence + 1) / len(frames))
|
|
158
|
+
report_progress(0.9)
|
|
159
|
+
self.packets = _reassemble(
|
|
160
|
+
fragments,
|
|
161
|
+
lambda progress: report_progress(0.9 + progress * 0.1),
|
|
162
|
+
)
|
|
163
|
+
report_progress(1.0)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_bytes(
|
|
167
|
+
cls,
|
|
168
|
+
data: bytes | bytearray,
|
|
169
|
+
decode_transform: Callable[[object], object] | None = None,
|
|
170
|
+
*,
|
|
171
|
+
progress_callback: Callable[[float], None] | None = None,
|
|
172
|
+
) -> "CaptureDocument":
|
|
173
|
+
"""Detect and decode a PCAP or PCAPNG byte stream."""
|
|
174
|
+
magic = bytes(data[:4])
|
|
175
|
+
if magic == b"\x0a\x0d\x0d\x0a":
|
|
176
|
+
return cls(data,
|
|
177
|
+
"pcapng",
|
|
178
|
+
decode_transform,
|
|
179
|
+
progress_callback=progress_callback)
|
|
180
|
+
if magic in {
|
|
181
|
+
b"\xd4\xc3\xb2\xa1",
|
|
182
|
+
b"\xa1\xb2\xc3\xd4",
|
|
183
|
+
b"\x4d\x3c\xb2\xa1",
|
|
184
|
+
b"\xa1\xb2\x3c\x4d",
|
|
185
|
+
}:
|
|
186
|
+
return cls(data,
|
|
187
|
+
"pcap",
|
|
188
|
+
decode_transform,
|
|
189
|
+
progress_callback=progress_callback)
|
|
190
|
+
raise ValueError("The file is not a supported PCAP or PCAPNG capture.")
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def open(
|
|
194
|
+
cls,
|
|
195
|
+
path: str | Path,
|
|
196
|
+
decode_transform: Callable[[object], object] | None = None,
|
|
197
|
+
*,
|
|
198
|
+
progress_callback: Callable[[float], None] | None = None,
|
|
199
|
+
) -> "CaptureDocument":
|
|
200
|
+
"""Open a capture document from the specified file path."""
|
|
201
|
+
return cls.from_bytes(
|
|
202
|
+
bytearray(Path(path).read_bytes()),
|
|
203
|
+
decode_transform,
|
|
204
|
+
progress_callback=progress_callback,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def save(self, path: str | Path) -> None:
|
|
208
|
+
"""Save the capture document to the specified file path."""
|
|
209
|
+
Path(path).write_bytes(self.data)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _frames(instance: object, capture_format: str) -> list[_Frame]:
|
|
213
|
+
"""Extract frame metadata from decoded PCAP or PCAPNG fields."""
|
|
214
|
+
if capture_format == "pcap":
|
|
215
|
+
return _pcap_frames(instance)
|
|
216
|
+
return _pcapng_frames(instance)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _field(instance: object, name: str) -> object | None:
|
|
220
|
+
"""Return a named direct child field instance, if present."""
|
|
221
|
+
return next((field for field in getattr(instance, "field_instances", ())
|
|
222
|
+
if field.field_def.name == name), None)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _field_offset(*fields: object) -> int:
|
|
226
|
+
"""Return the absolute byte offset represented by nested fields."""
|
|
227
|
+
return sum(field.field_def.offset.bytes for field in fields)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _timestamp_values(fields: tuple[object, ...]) -> dict[str, object]:
|
|
231
|
+
"""Extract timestamp values from the direct fields of a packet record."""
|
|
232
|
+
values = {}
|
|
233
|
+
for field in fields:
|
|
234
|
+
name = field.field_def.name
|
|
235
|
+
value = field.value
|
|
236
|
+
child_values = {
|
|
237
|
+
child.field_def.name: child.value
|
|
238
|
+
for child in getattr(value, "field_instances", ())
|
|
239
|
+
}
|
|
240
|
+
if name == "pcap_timestamp":
|
|
241
|
+
display_value = getattr(value, "display_value", None)
|
|
242
|
+
if display_value is not None:
|
|
243
|
+
values["timestamp"] = display_value
|
|
244
|
+
else:
|
|
245
|
+
seconds = child_values["timestamp_seconds"]
|
|
246
|
+
nanoseconds = child_values["timestamp_nanoseconds"]
|
|
247
|
+
values["timestamp"] = (seconds if isinstance(seconds, str) else
|
|
248
|
+
f"{seconds}.{nanoseconds:09d}")
|
|
249
|
+
elif name == "timestamp":
|
|
250
|
+
display_value = getattr(value, "display_value", None)
|
|
251
|
+
if display_value is not None:
|
|
252
|
+
values[name] = display_value
|
|
253
|
+
else:
|
|
254
|
+
high = child_values["high"]
|
|
255
|
+
low = child_values["low"]
|
|
256
|
+
values[name] = (high if isinstance(high, str) else high << 32
|
|
257
|
+
| low)
|
|
258
|
+
elif name.startswith("timestamp_"):
|
|
259
|
+
values[name] = value
|
|
260
|
+
return values
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def internet_checksum(data: bytes | bytearray | virtual_bytearray, ) -> int:
|
|
264
|
+
"""Compute the RFC 1071 one's-complement checksum for the given data."""
|
|
265
|
+
total = 0
|
|
266
|
+
high_byte = None
|
|
267
|
+
for value in data:
|
|
268
|
+
if high_byte is None:
|
|
269
|
+
high_byte = value
|
|
270
|
+
else:
|
|
271
|
+
total += high_byte << 8 | value
|
|
272
|
+
high_byte = None
|
|
273
|
+
if high_byte is not None:
|
|
274
|
+
total += high_byte << 8
|
|
275
|
+
while total >> 16:
|
|
276
|
+
total = (total & 0xFFFF) + (total >> 16)
|
|
277
|
+
return (~total) & 0xFFFF
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _pcap_frames(instance: object) -> list[_Frame]:
|
|
281
|
+
"""Extract PCAP frame metadata from decoded fields."""
|
|
282
|
+
header = _field(instance, "header")
|
|
283
|
+
if header is None:
|
|
284
|
+
return []
|
|
285
|
+
link_type = _field(header.value, "link_type").value & 0xFFFF
|
|
286
|
+
frames = []
|
|
287
|
+
for record in instance.field_instances:
|
|
288
|
+
if not record.field_def.name.startswith("record["):
|
|
289
|
+
continue
|
|
290
|
+
packet_data = _field(record.value, "packet_data")
|
|
291
|
+
if packet_data is not None:
|
|
292
|
+
frames.append(
|
|
293
|
+
(_field_offset(record,
|
|
294
|
+
packet_data), packet_data.field_def.size.bytes,
|
|
295
|
+
link_type, _timestamp_values(record.value.field_instances),
|
|
296
|
+
packet_data.value))
|
|
297
|
+
return frames
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _pcapng_frames(instance: object) -> list[_Frame]:
|
|
301
|
+
"""Extract PCAPNG frame metadata from decoded fields."""
|
|
302
|
+
frames = []
|
|
303
|
+
interfaces: list[tuple[int, int]] = []
|
|
304
|
+
for block in instance.field_instances:
|
|
305
|
+
if _field(block.value, "byte_order_magic") is not None:
|
|
306
|
+
interfaces = []
|
|
307
|
+
body = _field(block.value, "body")
|
|
308
|
+
if body is None:
|
|
309
|
+
continue
|
|
310
|
+
link_type = _field(body.value, "link_type")
|
|
311
|
+
snap_length = _field(body.value, "snap_len")
|
|
312
|
+
if link_type is not None and snap_length is not None:
|
|
313
|
+
interfaces.append((link_type.value, snap_length.value))
|
|
314
|
+
continue
|
|
315
|
+
packet_data = _field(body.value, "packet_data")
|
|
316
|
+
interface_id = _field(body.value, "interface_id")
|
|
317
|
+
if packet_data is not None and interface_id is not None:
|
|
318
|
+
if interface_id.value < len(interfaces):
|
|
319
|
+
frame_link_type, _ = interfaces[interface_id.value]
|
|
320
|
+
frames.append(
|
|
321
|
+
(_field_offset(block, body, packet_data),
|
|
322
|
+
packet_data.field_def.size.bytes, frame_link_type,
|
|
323
|
+
_timestamp_values(body.value.field_instances),
|
|
324
|
+
packet_data.value))
|
|
325
|
+
continue
|
|
326
|
+
packet_data = _field(body.value, "packet_data_with_padding")
|
|
327
|
+
original_length = _field(body.value, "original_packet_length")
|
|
328
|
+
if packet_data is None or original_length is None:
|
|
329
|
+
continue
|
|
330
|
+
if not interfaces:
|
|
331
|
+
continue
|
|
332
|
+
frame_link_type, snap_length = interfaces[0]
|
|
333
|
+
frames.append((_field_offset(block, body, packet_data),
|
|
334
|
+
min(original_length.value, snap_length,
|
|
335
|
+
packet_data.field_def.size.bytes), frame_link_type,
|
|
336
|
+
{}, packet_data.value))
|
|
337
|
+
return frames
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _extract_ip_fragment(
|
|
341
|
+
capture: bytearray,
|
|
342
|
+
frame_offset: int,
|
|
343
|
+
frame_length: int,
|
|
344
|
+
link_type: int,
|
|
345
|
+
sequence: int,
|
|
346
|
+
timestamps: dict[str, object],
|
|
347
|
+
packet_data: object,
|
|
348
|
+
) -> _RawFragment | None:
|
|
349
|
+
"""Extract an IP fragment from decoded Ethernet packet data."""
|
|
350
|
+
del frame_length
|
|
351
|
+
if link_type != 1:
|
|
352
|
+
return None
|
|
353
|
+
network_packet, network_offset = _network_packet(packet_data)
|
|
354
|
+
if network_packet is None:
|
|
355
|
+
return None
|
|
356
|
+
if _field(network_packet, "version_ihl") is not None:
|
|
357
|
+
return _extract_ipv4(capture, frame_offset + network_offset, sequence,
|
|
358
|
+
timestamps, network_packet)
|
|
359
|
+
if _field(network_packet, "version_traffic_flow") is not None:
|
|
360
|
+
return _extract_ipv6(capture, frame_offset + network_offset, sequence,
|
|
361
|
+
timestamps, network_packet)
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _network_packet(packet_data: object) -> tuple[object | None, int]:
|
|
366
|
+
"""Return the decoded IP packet and its offset within Ethernet data."""
|
|
367
|
+
offset = 0
|
|
368
|
+
current = packet_data
|
|
369
|
+
while True:
|
|
370
|
+
network_packet = _field(current, "network_packet")
|
|
371
|
+
if network_packet is None:
|
|
372
|
+
return None, offset
|
|
373
|
+
offset += network_packet.field_def.offset.bytes
|
|
374
|
+
current = network_packet.value
|
|
375
|
+
if (_field(current, "version_ihl") is not None
|
|
376
|
+
or _field(current, "version_traffic_flow") is not None):
|
|
377
|
+
return current, offset
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _extract_ipv4(capture: bytearray, offset: int, sequence: int,
|
|
381
|
+
timestamps: dict[str, object],
|
|
382
|
+
packet: object) -> _RawFragment | None:
|
|
383
|
+
"""Extract an IPv4 fragment from decoded packet fields."""
|
|
384
|
+
transport_segment = _field(packet, "transport_segment")
|
|
385
|
+
if transport_segment is None:
|
|
386
|
+
return None
|
|
387
|
+
fragment_offset = _field(packet, "fragment_offset").value * 8
|
|
388
|
+
more_fragments = bool(_field(packet, "more_fragments").value)
|
|
389
|
+
protocol = _field(packet, "protocol").value
|
|
390
|
+
identification = _field(packet, "identification").value
|
|
391
|
+
payload_start = offset + transport_segment.field_def.offset.bytes
|
|
392
|
+
return _raw_fragment(capture, sequence, timestamps, packet, 4, protocol,
|
|
393
|
+
identification, fragment_offset, more_fragments,
|
|
394
|
+
payload_start, transport_segment.field_def.size.bytes,
|
|
395
|
+
fragment_offset > 0 or more_fragments)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _extract_ipv6(capture: bytearray, offset: int, sequence: int,
|
|
399
|
+
timestamps: dict[str, object],
|
|
400
|
+
packet: object) -> _RawFragment | None:
|
|
401
|
+
"""Extract an IPv6 fragment from decoded packet fields."""
|
|
402
|
+
transport_segment = _field(packet, "transport_segment")
|
|
403
|
+
if transport_segment is None:
|
|
404
|
+
return None
|
|
405
|
+
if _field(packet, "next_header").value == 44:
|
|
406
|
+
fragment_header = transport_segment.value
|
|
407
|
+
payload_field = _field(fragment_header, "fragment_payload")
|
|
408
|
+
if payload_field is None:
|
|
409
|
+
return None
|
|
410
|
+
protocol = _field(fragment_header, "next_header").value
|
|
411
|
+
fragment_offset = _field(fragment_header, "fragment_offset").value * 8
|
|
412
|
+
more_field = _field(fragment_header, "more_fragments")
|
|
413
|
+
more_fragments = bool(more_field.value)
|
|
414
|
+
identification = _field(fragment_header, "identification").value
|
|
415
|
+
payload_start = (offset + transport_segment.field_def.offset.bytes +
|
|
416
|
+
payload_field.field_def.offset.bytes)
|
|
417
|
+
else:
|
|
418
|
+
protocol = _field(packet, "next_header").value
|
|
419
|
+
fragment_offset = 0
|
|
420
|
+
more_fragments = False
|
|
421
|
+
identification = None
|
|
422
|
+
payload_field = transport_segment
|
|
423
|
+
payload_start = offset + transport_segment.field_def.offset.bytes
|
|
424
|
+
return _raw_fragment(capture, sequence, timestamps, packet, 6, protocol,
|
|
425
|
+
identification, fragment_offset, more_fragments,
|
|
426
|
+
payload_start, payload_field.field_def.size.bytes,
|
|
427
|
+
identification is not None)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _raw_fragment(
|
|
431
|
+
capture: bytearray,
|
|
432
|
+
sequence: int,
|
|
433
|
+
timestamps: dict[str, object],
|
|
434
|
+
packet: object,
|
|
435
|
+
ip_version: int,
|
|
436
|
+
protocol: int,
|
|
437
|
+
identification: int | None,
|
|
438
|
+
fragment_offset: int,
|
|
439
|
+
more_fragments: bool,
|
|
440
|
+
payload_start: int,
|
|
441
|
+
payload_length: int,
|
|
442
|
+
is_fragmented: bool,
|
|
443
|
+
) -> _RawFragment:
|
|
444
|
+
"""Build a raw fragment backed by the original capture data."""
|
|
445
|
+
source = bytes(_field(packet, "source_address").value)
|
|
446
|
+
destination = bytes(_field(packet, "destination_address").value)
|
|
447
|
+
if is_fragmented:
|
|
448
|
+
key = (ip_version, source, destination, protocol, identification)
|
|
449
|
+
else:
|
|
450
|
+
key = (ip_version, sequence)
|
|
451
|
+
payload = virtual_bytearray(
|
|
452
|
+
(capture, ))[payload_start:payload_start + payload_length]
|
|
453
|
+
return _RawFragment(sequence, timestamps, key, ip_version, source,
|
|
454
|
+
destination, protocol, identification, fragment_offset,
|
|
455
|
+
more_fragments, payload_start, payload)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _reassemble(
|
|
459
|
+
fragments: list[_RawFragment],
|
|
460
|
+
progress_callback: Callable[[float], None] | None = None,
|
|
461
|
+
) -> list[ReassembledPacket]:
|
|
462
|
+
"""Reassemble raw IP fragments into complete packets."""
|
|
463
|
+
groups: dict[tuple, list[_RawFragment]] = {}
|
|
464
|
+
for fragment in fragments:
|
|
465
|
+
groups.setdefault(fragment.key, []).append(fragment)
|
|
466
|
+
|
|
467
|
+
packets = []
|
|
468
|
+
for group_index, group in enumerate(groups.values()):
|
|
469
|
+
group.sort(key=lambda fragment: (fragment.offset, fragment.sequence))
|
|
470
|
+
expected_length = max(
|
|
471
|
+
(fragment.offset + len(fragment.data)
|
|
472
|
+
for fragment in group if not fragment.more_fragments),
|
|
473
|
+
default=None)
|
|
474
|
+
data_length = expected_length or max(
|
|
475
|
+
fragment.offset + len(fragment.data) for fragment in group)
|
|
476
|
+
chunks = []
|
|
477
|
+
cursor = 0
|
|
478
|
+
has_gap = False
|
|
479
|
+
locations = []
|
|
480
|
+
for fragment in group:
|
|
481
|
+
fragment_end = min(data_length,
|
|
482
|
+
fragment.offset + len(fragment.data))
|
|
483
|
+
length = fragment_end - fragment.offset
|
|
484
|
+
locations.append(
|
|
485
|
+
PacketFragment(fragment.sequence, fragment.timestamps,
|
|
486
|
+
fragment.capture_offset, fragment.offset, length,
|
|
487
|
+
fragment.more_fragments))
|
|
488
|
+
if fragment.offset > cursor:
|
|
489
|
+
chunks.append(bytes(fragment.offset - cursor))
|
|
490
|
+
has_gap = True
|
|
491
|
+
cursor = fragment.offset
|
|
492
|
+
if fragment_end > cursor:
|
|
493
|
+
chunk_start = cursor - fragment.offset
|
|
494
|
+
chunks.append(fragment.data[chunk_start:length])
|
|
495
|
+
cursor = fragment_end
|
|
496
|
+
if cursor < data_length:
|
|
497
|
+
chunks.append(bytes(data_length - cursor))
|
|
498
|
+
has_gap = True
|
|
499
|
+
data = virtual_bytearray(chunks)
|
|
500
|
+
first = group[0]
|
|
501
|
+
packets.append(
|
|
502
|
+
ReassembledPacket(
|
|
503
|
+
sequence=min(fragment.sequence for fragment in group),
|
|
504
|
+
timestamps=first.timestamps,
|
|
505
|
+
key=first.key,
|
|
506
|
+
ip_version=first.ip_version,
|
|
507
|
+
source_bytes=first.source,
|
|
508
|
+
destination_bytes=first.destination,
|
|
509
|
+
protocol=first.protocol,
|
|
510
|
+
identification=first.identification,
|
|
511
|
+
fragments=locations,
|
|
512
|
+
data=data,
|
|
513
|
+
complete=expected_length is not None and not has_gap,
|
|
514
|
+
))
|
|
515
|
+
if progress_callback is not None:
|
|
516
|
+
progress_callback((group_index + 1) / len(groups))
|
|
517
|
+
packets.sort(key=lambda packet: packet.sequence)
|
|
518
|
+
return packets
|