bdo-toolkit 1.0.0__tar.gz → 1.0.2__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.
- {bdo_toolkit-1.0.0/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.2}/PKG-INFO +6 -5
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/README.md +3 -3
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/pyproject.toml +5 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_async_sessions.py +13 -18
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_backend.py +121 -4
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_runtime.py +15 -17
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_deposit_origin.py +579 -155
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_engine.py +9 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_framing.py +29 -7
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_protocol.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_reassembly.py +20 -7
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_storage_hydration.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_version.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/calibration.py +11 -13
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/capture.py +24 -22
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/character_state.py +20 -6
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/events.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/filters.py +2 -5
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/origin_learning.py +3 -3
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/remote_profiles.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_discovery.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/async_session.py +10 -18
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/models.py +1 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/session.py +5 -6
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2/src/bdo_toolkit.egg-info}/PKG-INFO +6 -5
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/SOURCES.txt +1 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_async_sessions.py +1 -4
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration_lifecycle.py +17 -6
- bdo_toolkit-1.0.2/tests/test_capture_backend.py +446 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_capture_runtime.py +4 -5
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_character_state.py +179 -3
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_deposit_origin.py +834 -7
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_inventory_snapshots.py +43 -1
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_reassembly.py +133 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_session.py +13 -16
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/LICENSE +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/setup.cfg +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/__init__.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/cli.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/item_state.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration_conveniences.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration_retention.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_decoder_diagnostics.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_direction.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_examples_and_writers.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_explicit_profile.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_fixture_regressions.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_hardening.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_initial_load_storage.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_item_state.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_live_capture_session.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_origin_learning.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_learning.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_real_captures.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_replay.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_events.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_hydration_tracker.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_unknown_destination_calibration.py +0 -0
- {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_toolkit_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bdo-toolkit
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Passive, read-only BDO packet parsing toolkit for app developers.
|
|
5
5
|
Author: bdo-toolkit contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,9 +12,10 @@ Keywords: bdo,packets,parse,pcap,passive
|
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
17
|
Classifier: Typing :: Typed
|
|
17
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.14
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
License-File: LICENSE
|
|
20
21
|
Requires-Dist: scapy>=2.5
|
|
@@ -27,7 +28,7 @@ Dynamic: license-file
|
|
|
27
28
|
# bdo-toolkit
|
|
28
29
|
|
|
29
30
|
[](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
|
|
30
|
-
[](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
|
|
31
32
|

|
|
32
33
|
[](https://ychwu.github.io/bdo-toolkit/#stability)
|
|
33
34
|
[](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
|
|
@@ -62,7 +63,7 @@ services is unknown.
|
|
|
62
63
|
python -m pip install bdo-toolkit
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
Python 3.
|
|
66
|
+
Python 3.14 or newer is required. Before live capture, complete
|
|
66
67
|
[Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
|
|
67
68
|
including Npcap on Windows and permission to capture on the selected interface.
|
|
68
69
|
Offline PCAP and PCAPNG replay does not require Npcap.
|
|
@@ -134,7 +135,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
|
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
CI runs tests, type checking, wheel construction, and a CLI smoke test on
|
|
137
|
-
Ubuntu and Windows with Python 3.
|
|
138
|
+
Ubuntu and Windows with Python 3.14. Tests that require private
|
|
138
139
|
game-session captures skip when those local fixtures are absent.
|
|
139
140
|
|
|
140
141
|
## License
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bdo-toolkit
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
|
|
4
|
-
[](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
|
|
5
5
|

|
|
6
6
|
[](https://ychwu.github.io/bdo-toolkit/#stability)
|
|
7
7
|
[](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
|
|
@@ -36,7 +36,7 @@ services is unknown.
|
|
|
36
36
|
python -m pip install bdo-toolkit
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Python 3.
|
|
39
|
+
Python 3.14 or newer is required. Before live capture, complete
|
|
40
40
|
[Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
|
|
41
41
|
including Npcap on Windows and permission to capture on the selected interface.
|
|
42
42
|
Offline PCAP and PCAPNG replay does not require Npcap.
|
|
@@ -108,7 +108,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
|
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
CI runs tests, type checking, wheel construction, and a CLI smoke test on
|
|
111
|
-
Ubuntu and Windows with Python 3.
|
|
111
|
+
Ubuntu and Windows with Python 3.14. Tests that require private
|
|
112
112
|
game-session captures skip when those local fixtures are absent.
|
|
113
113
|
|
|
114
114
|
## License
|
|
@@ -7,7 +7,7 @@ name = "bdo-toolkit"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Passive, read-only BDO packet parsing toolkit for app developers."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"scapy>=2.5",
|
|
13
13
|
]
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Development Status :: 5 - Production/Stable",
|
|
22
22
|
"Operating System :: OS Independent",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
24
25
|
"Programming Language :: Python :: 3 :: Only",
|
|
25
26
|
"Typing :: Typed",
|
|
26
27
|
]
|
|
@@ -47,6 +48,9 @@ where = ["src"]
|
|
|
47
48
|
[tool.pytest.ini_options]
|
|
48
49
|
testpaths = ["tests"]
|
|
49
50
|
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.14"
|
|
53
|
+
|
|
50
54
|
[tool.setuptools.package-data]
|
|
51
55
|
bdo_toolkit = ["py.typed"]
|
|
52
56
|
|
|
@@ -14,7 +14,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
14
14
|
from functools import partial
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from types import TracebackType
|
|
17
|
-
from typing import AsyncIterator, Callable, Optional
|
|
17
|
+
from typing import AsyncIterator, Callable, Optional
|
|
18
18
|
|
|
19
19
|
from ._capture_runtime import CaptureEndpoint, _attach_cleanup_owner
|
|
20
20
|
from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
|
|
@@ -33,10 +33,7 @@ from .origin_learning import CompanionObservation
|
|
|
33
33
|
from .profiles import OpcodeProfile
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
async def _wait_ignoring_cancellation(future: asyncio.Future[T]) -> T:
|
|
36
|
+
async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
|
|
40
37
|
"""Wait for an already-started operation, even after caller cancellation."""
|
|
41
38
|
|
|
42
39
|
while not future.done():
|
|
@@ -51,14 +48,14 @@ async def _wait_ignoring_cancellation(future: asyncio.Future[T]) -> T:
|
|
|
51
48
|
return future.result()
|
|
52
49
|
|
|
53
50
|
|
|
54
|
-
async def _await_preserving_future(future: asyncio.Future[T]) -> T:
|
|
51
|
+
async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
|
|
55
52
|
"""Await without cancelling or wrapping the submitted worker future."""
|
|
56
53
|
|
|
57
54
|
await asyncio.wait((future,))
|
|
58
55
|
return future.result()
|
|
59
56
|
|
|
60
57
|
|
|
61
|
-
def _thread_task(function: Callable[[], T]) -> asyncio.Task[T]:
|
|
58
|
+
def _thread_task[T](function: Callable[[], T]) -> asyncio.Task[T]:
|
|
62
59
|
return asyncio.create_task(asyncio.to_thread(function))
|
|
63
60
|
|
|
64
61
|
|
|
@@ -143,7 +140,7 @@ class AsyncLiveCaptureSession:
|
|
|
143
140
|
|
|
144
141
|
self._session.raise_if_failed()
|
|
145
142
|
|
|
146
|
-
def _submit(self, function: Callable[[], T]) -> asyncio.Future[T]:
|
|
143
|
+
def _submit[T](self, function: Callable[[], T]) -> asyncio.Future[T]:
|
|
147
144
|
if self._executor_closed:
|
|
148
145
|
raise RuntimeError("async live capture session is already closed")
|
|
149
146
|
loop = asyncio.get_running_loop()
|
|
@@ -380,11 +377,10 @@ class AsyncLiveCaptureSession:
|
|
|
380
377
|
self,
|
|
381
378
|
context="async live item capture context",
|
|
382
379
|
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
380
|
+
exc_value.add_note(
|
|
381
|
+
"async live item capture context cleanup also failed: "
|
|
382
|
+
f"{cleanup_error!r}"
|
|
383
|
+
)
|
|
388
384
|
|
|
389
385
|
|
|
390
386
|
class AsyncCalibrationSession:
|
|
@@ -644,8 +640,7 @@ class AsyncCalibrationSession:
|
|
|
644
640
|
self,
|
|
645
641
|
context="async live calibration context",
|
|
646
642
|
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
)
|
|
643
|
+
exc_value.add_note(
|
|
644
|
+
"async live calibration context cleanup also failed: "
|
|
645
|
+
f"{cleanup_error!r}"
|
|
646
|
+
)
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Iterable, Iterator, Optional, Protocol
|
|
7
|
+
from typing import Any, Iterable, Iterator, Optional, Protocol
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SegmentConsumer(Protocol):
|
|
@@ -93,21 +93,138 @@ def validate_server_ports(ports: Iterable[int]) -> tuple[int, ...]:
|
|
|
93
93
|
return tuple(normalized)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def _required_header_integer(value: Any, field_name: str) -> int:
|
|
97
|
+
"""Return one decoded header field or reject an incomplete dissection."""
|
|
98
|
+
if value is None:
|
|
99
|
+
raise ValueError(f"IPv4/TCP {field_name} is unavailable")
|
|
100
|
+
try:
|
|
101
|
+
return int(value)
|
|
102
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
103
|
+
raise ValueError(f"IPv4/TCP {field_name} is invalid: {value!r}") from exc
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_unfragmented_ipv4_tcp(ip: Any) -> None:
|
|
107
|
+
"""Reject IP states that cannot be handed to TCP reassembly safely."""
|
|
108
|
+
version = _required_header_integer(getattr(ip, "version", None), "version")
|
|
109
|
+
if version != 4:
|
|
110
|
+
raise ValueError(f"expected IPv4 version 4, got {version}")
|
|
111
|
+
|
|
112
|
+
fragment_offset = _required_header_integer(
|
|
113
|
+
getattr(ip, "frag", None),
|
|
114
|
+
"fragment offset",
|
|
115
|
+
)
|
|
116
|
+
flags = _required_header_integer(getattr(ip, "flags", None), "flags")
|
|
117
|
+
if fragment_offset != 0 or flags & 0x01:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"fragmented IPv4/TCP packets are unsupported; "
|
|
120
|
+
"TCP reassembly requires a complete IP datagram"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _captured_ipv4_length(ip: Any) -> int:
|
|
125
|
+
"""Return captured bytes without reserializing ordinary Scapy packets."""
|
|
126
|
+
original = getattr(ip, "original", None)
|
|
127
|
+
if isinstance(original, (bytes, bytearray, memoryview)) and original:
|
|
128
|
+
return len(original)
|
|
129
|
+
return len(bytes(ip))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _extract_ipv4_tcp_payload(ip: Any, tcp: Any) -> bytes:
|
|
133
|
+
"""Return only application bytes declared by complete IPv4/TCP headers.
|
|
134
|
+
|
|
135
|
+
Scapy exposes link-layer padding through ``tcp.payload`` even though those
|
|
136
|
+
bytes lie beyond the IPv4 total length. The wire header lengths are the
|
|
137
|
+
authority at this boundary so padding can never advance TCP sequence state.
|
|
138
|
+
"""
|
|
139
|
+
total_length = _required_header_integer(
|
|
140
|
+
getattr(ip, "len", None),
|
|
141
|
+
"total length",
|
|
142
|
+
)
|
|
143
|
+
ip_header_words = _required_header_integer(
|
|
144
|
+
getattr(ip, "ihl", None),
|
|
145
|
+
"header length",
|
|
146
|
+
)
|
|
147
|
+
tcp_header_words = _required_header_integer(
|
|
148
|
+
getattr(tcp, "dataofs", None),
|
|
149
|
+
"TCP header length",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not 5 <= ip_header_words <= 15:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"invalid IPv4 header length: {ip_header_words} 32-bit words"
|
|
155
|
+
)
|
|
156
|
+
if not 5 <= tcp_header_words <= 15:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"invalid TCP header length: {tcp_header_words} 32-bit words"
|
|
159
|
+
)
|
|
160
|
+
if not 0 <= total_length <= 0xFFFF:
|
|
161
|
+
raise ValueError(f"invalid IPv4 total length: {total_length}")
|
|
162
|
+
|
|
163
|
+
captured_length = _captured_ipv4_length(ip)
|
|
164
|
+
if captured_length < total_length:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
"truncated IPv4 packet: "
|
|
167
|
+
f"header declares {total_length} bytes, capture has {captured_length}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
header_length = (ip_header_words + tcp_header_words) * 4
|
|
171
|
+
if total_length < header_length:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"invalid IPv4/TCP header lengths: "
|
|
174
|
+
f"total length {total_length} is smaller than {header_length}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
declared_length = total_length - header_length
|
|
178
|
+
available = bytes(getattr(tcp, "payload", b""))
|
|
179
|
+
if len(available) < declared_length:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"truncated IPv4/TCP payload: "
|
|
182
|
+
f"header declares {declared_length} bytes, capture has {len(available)}"
|
|
183
|
+
)
|
|
184
|
+
return available[:declared_length]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _consumer_server_ports(engine: SegmentConsumer) -> Optional[frozenset[int]]:
|
|
188
|
+
"""Read the validated port set exposed by each built-in consumer."""
|
|
189
|
+
ports = getattr(engine, "server_ports", None)
|
|
190
|
+
if ports is None:
|
|
191
|
+
# Solare's internal collector predates the shared ``server_ports`` name.
|
|
192
|
+
ports = getattr(engine, "ports", None)
|
|
193
|
+
if ports is None:
|
|
194
|
+
return None
|
|
195
|
+
return frozenset(ports)
|
|
196
|
+
|
|
197
|
+
|
|
96
198
|
def make_packet_handler(engine: SegmentConsumer):
|
|
97
199
|
IP, TCP, _, _, _ = import_scapy()
|
|
200
|
+
server_ports = _consumer_server_ports(engine)
|
|
98
201
|
|
|
99
202
|
def handle(packet) -> None:
|
|
100
|
-
if IP not in packet
|
|
203
|
+
if IP not in packet:
|
|
101
204
|
return
|
|
102
205
|
|
|
103
206
|
ip = packet[IP]
|
|
207
|
+
protocol = _required_header_integer(getattr(ip, "proto", None), "protocol")
|
|
208
|
+
if protocol != 6:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if TCP not in packet:
|
|
212
|
+
# Non-initial fragments and severely truncated headers have no
|
|
213
|
+
# source port, so they cannot be attributed to a selected flow.
|
|
214
|
+
return
|
|
215
|
+
|
|
104
216
|
tcp = packet[TCP]
|
|
105
|
-
|
|
217
|
+
source_port = int(tcp.sport)
|
|
218
|
+
if server_ports is not None and source_port not in server_ports:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
_validate_unfragmented_ipv4_tcp(ip)
|
|
222
|
+
payload = _extract_ipv4_tcp_payload(ip, tcp)
|
|
106
223
|
flags = int(tcp.flags)
|
|
107
224
|
|
|
108
225
|
engine.process_tcp_segment(
|
|
109
226
|
source_ip=str(ip.src),
|
|
110
|
-
source_port=
|
|
227
|
+
source_port=source_port,
|
|
111
228
|
destination_ip=str(ip.dst),
|
|
112
229
|
destination_port=int(tcp.dport),
|
|
113
230
|
sequence=int(tcp.seq),
|
|
@@ -64,18 +64,17 @@ def _attach_cleanup_owner(
|
|
|
64
64
|
# Built-in and ordinary application exceptions expose __dict__. Keep
|
|
65
65
|
# the original failure identity even for an exotic immutable subtype.
|
|
66
66
|
pass
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
67
|
+
if attached:
|
|
68
|
+
error.add_note(
|
|
69
|
+
f"{context} cleanup is incomplete; retry stop() on "
|
|
70
|
+
"exception.cleanup_owner"
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
error.add_note(
|
|
74
|
+
f"{context} cleanup is incomplete; this exception subtype "
|
|
75
|
+
"cannot expose cleanup_owner, so retain and stop the original "
|
|
76
|
+
"session object"
|
|
77
|
+
)
|
|
79
78
|
|
|
80
79
|
|
|
81
80
|
@dataclass(frozen=True)
|
|
@@ -456,11 +455,10 @@ class LivePacketCapture:
|
|
|
456
455
|
# Startup remains the primary failure, but incomplete
|
|
457
456
|
# cleanup must be explicit in its traceback and remains
|
|
458
457
|
# retryable through stop().
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
)
|
|
458
|
+
exc.add_note(
|
|
459
|
+
"live capture startup cleanup also failed: "
|
|
460
|
+
f"{cleanup_error!r}"
|
|
461
|
+
)
|
|
464
462
|
if not self._cleanup_incomplete:
|
|
465
463
|
self._capture = None
|
|
466
464
|
self._capture_socket = None
|