network-sandbox-engine 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.
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.4
2
+ Name: network-sandbox-engine
3
+ Version: 1.0.0
4
+ Summary: Headless Python engine for isolated network namespace testing and PCAP assertions.
5
+ Author: onyks
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/onyks-os/NetworkSandboxEngine
8
+ Project-URL: Repository, https://github.com/onyks-os/NetworkSandboxEngine
9
+ Project-URL: Issues, https://github.com/onyks-os/NetworkSandboxEngine/issues
10
+ Keywords: networking,namespace,testing,nftables,pcap,scapy
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: System :: Networking
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: scapy>=2.5.0
25
+ Provides-Extra: cli
26
+ Requires-Dist: pydantic>=2.0.0; extra == "cli"
27
+ Requires-Dist: pyyaml>=6.0; extra == "cli"
28
+ Provides-Extra: gui
29
+ Requires-Dist: fastapi>=0.111.0; extra == "gui"
30
+ Requires-Dist: uvicorn[standard]>=0.29.0; extra == "gui"
31
+ Requires-Dist: websockets>=12.0; extra == "gui"
32
+ Requires-Dist: pydantic>=2.0.0; extra == "gui"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
35
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
36
+ Requires-Dist: ruff; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # Network Sandbox Engine (NSE)
40
+
41
+ A headless Python engine for deterministic, kernel-level `nftables` firewall testing, with an optional Svelte/FastAPI web GUI.
42
+
43
+ [![PyPI](https://img.shields.io/badge/PyPI-network--sandbox--engine-blue)](https://pypi.org/project/network-sandbox-engine/)
44
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
46
+ [![Linux only](https://img.shields.io/badge/OS-Linux%20only-lightgrey.svg)](https://kernel.org/)
47
+
48
+ NSE uses ephemeral Linux network namespaces and Scapy to validate firewall logic. Rules are executed by the actual kernel: no userspace simulation, no host pollution.
49
+
50
+ ---
51
+
52
+ ## How It Works
53
+
54
+ ```text
55
+ [Library / CLI] [FastAPI Daemon (optional)] [Sandbox - Linux netns]
56
+ | | |
57
+ |-- run_test_pipeline() ------>| |
58
+ | (rules + packet sequence) |-- 1. Create topology ------->| (veth pair / gateway)
59
+ | |-- 2. Spawn mock listeners -->| (TCP/UDP echo daemons)
60
+ | |-- 3. Load nft rules -------->|
61
+ | |-- 4. Start nft monitor ----->| (trace harvester)
62
+ | |-- 5. Inject packets -------->| (Scapy L2/L3)
63
+ | |<- 6. Poll conntrack ---------| (/proc/net/nf_conntrack)
64
+ |<-- TraceEvent stream --------|-- 7. Stream verdicts ------->| (WebSocket or iterator)
65
+ | |-- 8. Teardown (GC) --------->| (namespace deleted)
66
+ ```
67
+
68
+ The host firewall is never modified. Rules are confined to the isolated sandbox namespace and are destroyed with it at teardown.
69
+
70
+ ---
71
+
72
+ ## Architecture Overview
73
+
74
+ | Component | Technology | Description |
75
+ | ---------------------- | ------------------------ | ----------------------------------------------------------------------------- |
76
+ | Headless Core | Python 3.10+ / Scapy | `NetnsController`, `PCAPAsserter`, `RuleEngine`, `ScapyInjector`, `Pipeline` |
77
+ | CLI Test Runner | `nse-runner` / YAML | Headless YAML test suite runner for CI/CD pipelines |
78
+ | GUI Daemon (optional) | FastAPI + Uvicorn | REST and WebSocket API streaming `TraceEvent` objects from the kernel |
79
+ | Frontend (optional) | Svelte + Vite | Rule editor, multi-packet crafter, animated trace visualizer, conntrack table |
80
+ | Packet Injection | Scapy (Layer 2/3) | IPv4 and IPv6, TCP with custom flags, UDP, ICMP, ICMPv6 |
81
+ | Mock Listeners | TCP/UDP echo sockets | Background listeners inside namespaces to complete handshakes |
82
+ | Conntrack Engine | `/proc/net/nf_conntrack` | Captures `ESTABLISHED`, `SYN_SENT`, `TIME_WAIT` states in real-time |
83
+
84
+ ---
85
+
86
+ ## Prerequisites
87
+
88
+ NSE requires Linux with kernel 5.4 or later (namespace and nftables trace support).
89
+
90
+ | Dependency | Purpose |
91
+ | ----------------- | ------------------------------------------ |
92
+ | Python 3.10+ | Core library, CLI, and optional GUI daemon |
93
+ | nftables (`nft`) | Compiles rules and generates trace events |
94
+ | iproute2 (`ip`) | Manages network namespaces and veth pairs |
95
+ | conntrack | Reads connection state from kernel tables |
96
+ | Node.js 18+ | Required only to build the Svelte frontend |
97
+
98
+ ```bash
99
+ # Debian / Ubuntu
100
+ sudo apt install nftables iproute2 python3-venv python3-pip conntrack
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Quickstart
106
+
107
+ ### A. Headless library
108
+
109
+ ```bash
110
+ pip install network-sandbox-engine
111
+ ```
112
+
113
+ ```python
114
+ import asyncio
115
+ from nse.core.pipeline import run_test_pipeline
116
+ from nse.models.test_request import PacketSpec, TestRequest
117
+
118
+ async def main():
119
+ req = TestRequest(
120
+ rules="table ip filter { chain input { type filter hook input priority 0; tcp dport 22 accept; drop; } }",
121
+ packets=[PacketSpec(protocol="tcp", src_ip="10.0.0.1", dst_ip="10.0.0.2", dst_port=22)],
122
+ )
123
+ events = await run_test_pipeline(req)
124
+ for ev in events:
125
+ print(ev)
126
+
127
+ asyncio.run(main())
128
+ ```
129
+
130
+ ### B. CLI YAML runner
131
+
132
+ ```bash
133
+ pip install "network-sandbox-engine[cli]"
134
+ nse-runner --file my_tests.yaml
135
+ ```
136
+
137
+ ```yaml
138
+ # my_tests.yaml
139
+ - name: SSH accepted
140
+ rules: |
141
+ table ip filter {
142
+ chain input { type filter hook input priority 0; tcp dport 22 accept; drop; }
143
+ }
144
+ packets:
145
+ - protocol: tcp
146
+ src_ip: 10.0.0.1
147
+ dst_ip: 10.0.0.2
148
+ dst_port: 22
149
+ expect:
150
+ verdict: ACCEPT
151
+ ```
152
+
153
+ ### C. Full GUI (development mode)
154
+
155
+ ```bash
156
+ git clone https://github.com/onyks-os/NetworkSandboxEngine.git
157
+ cd NetworkSandboxEngine
158
+ make setup # bootstrap venv + npm install
159
+ make backend # starts FastAPI daemon (requires sudo -E)
160
+ make frontend # starts Vite dev server on port 5173
161
+ ```
162
+
163
+ Open `http://localhost:5173` in a browser.
164
+
165
+ ---
166
+
167
+ ## Repository Layout
168
+
169
+ ```
170
+ NetworkSandboxEngine/
171
+ |-- nse/ # PyPI package (pip install network-sandbox-engine)
172
+ | |-- __init__.py # Public API: NetnsController, PCAPAsserter
173
+ | |-- core/ # Kernel-level primitives
174
+ | | |-- netns_controller.py
175
+ | | |-- scapy_injector.py
176
+ | | |-- sniffer.py # PCAPAsserter
177
+ | | |-- pipeline.py # run_test_pipeline()
178
+ | | `-- rule_engine.py # nft load / validate
179
+ | |-- models/ # Pydantic models (lazy import via try/except)
180
+ | | |-- test_request.py # PacketSpec, TestRequest, TopologyType
181
+ | | `-- trace_event.py # TraceEvent
182
+ | `-- cli/
183
+ | `-- runner.py # nse-runner entrypoint
184
+ |
185
+ |-- gui/ # Not on PyPI - GUI daemon only
186
+ | |-- server.py # FastAPI + Uvicorn entrypoint
187
+ | |-- api/ # REST routes and WebSocket
188
+ | `-- daemon/ # trace_harvester, mock_listener
189
+ | `-- gui_svelte/ # Svelte + Vite frontend
190
+ |
191
+ |-- tests/
192
+ | `-- test_netns.py # 20 unit tests (2 skipped without root)
193
+ |
194
+ |-- pyproject.toml # Build config - packages only nse/
195
+ |-- Makefile # make setup | test | lint | release
196
+ `-- conftest.py # Root sys.path for pytest
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Key Features
202
+
203
+ ### 1. Stateful Packet Sequences and Conntrack
204
+
205
+ Ordered lists of packets simulate TCP flows. NSE polls `/proc/net/nf_conntrack` and streams live connection states (`SYN_SENT`, `ESTABLISHED`, `TIME_WAIT`) after each injection.
206
+
207
+ ### 2. Automatic Mock Listeners
208
+
209
+ Destination ports in incoming packets receive a background echo listener spawned inside the namespace, completing TCP handshakes and generating valid conntrack entries without manual setup.
210
+
211
+ ### 3. Gateway Routing Topology
212
+
213
+ The Gateway Topology spawns a three-namespace chain: Host - Router - Server. Rules are loaded into the Router namespace to test `forward` chain hooks, routing decisions, and NAT.
214
+
215
+ ### 4. Dual-Stack IPv4 and IPv6
216
+
217
+ ICMPv6 echo, dual-stack veth links, and DAD disabled for instant address availability inside namespaces.
218
+
219
+ ### 5. PCAP Assertions
220
+
221
+ `PCAPAsserter` wraps Scapy's `AsyncSniffer` to arm a BPF filter on a veth interface and assert captured packets. It is usable independently of the full pipeline.
222
+
223
+ ---
224
+
225
+ ## Testing
226
+
227
+ Unit tests (no root required):
228
+
229
+ ```bash
230
+ make test
231
+ # 20 passed, 2 skipped (root-only integration tests)
232
+ ```
233
+
234
+ Integration tests (root required):
235
+
236
+ ```bash
237
+ sudo -E .venv/bin/pytest tests/ -v
238
+ ```
239
+
240
+ CLI test suite (root required):
241
+
242
+ ```bash
243
+ sudo -E .venv/bin/python -m nse.cli.runner --file tests/fixtures/test_suite.yaml
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Production Deployment
249
+
250
+ ### Package Extras
251
+
252
+ | Mode | Install command | Dependencies |
253
+ | :------------- | :------------------------------------------ | :--------------------- |
254
+ | Headless core | `pip install network-sandbox-engine` | `scapy` |
255
+ | With CLI runner | `pip install "network-sandbox-engine[cli]"` | + `pydantic`, `pyyaml` |
256
+
257
+ The GUI daemon is not distributed via PyPI. It is run from a repository clone.
258
+
259
+ ### Building Release Artifacts
260
+
261
+ ```bash
262
+ make release
263
+ ```
264
+
265
+ This target performs the following steps:
266
+
267
+ 1. Runs `make lint` and `make test`; fails on any error.
268
+ 2. Builds `.whl` and `.tar.gz` with `python -m build`.
269
+ 3. Copies `Dockerfile` and `scripts/nse.service` into `release/`.
270
+ 4. Generates `SHA256SUMS`.
271
+ 5. Signs `SHA256SUMS` with GPG, producing `SHA256SUMS.asc`. The signing key is auto-detected from the keyring; it can be overridden with `GPG_KEY_ID=<id>`.
272
+
273
+ Output in `release/`:
274
+
275
+ ```
276
+ release/
277
+ |-- network_sandbox_engine-1.0.0-py3-none-any.whl
278
+ |-- network_sandbox_engine-1.0.0.tar.gz
279
+ |-- Dockerfile
280
+ |-- nse.service
281
+ |-- SHA256SUMS
282
+ `-- SHA256SUMS.asc
283
+ ```
284
+
285
+ ### Native Installation with Systemd
286
+
287
+ ```bash
288
+ sudo pip install release/network_sandbox_engine-1.0.0-py3-none-any.whl
289
+ sudo cp release/nse.service /etc/systemd/system/
290
+ sudo systemctl daemon-reload && sudo systemctl enable --now nse
291
+ ```
292
+
293
+ ### Docker
294
+
295
+ ```bash
296
+ docker build -t nse -f release/Dockerfile .
297
+ docker run --privileged -p 8000:8000 -d --name nse-container nse
298
+ ```
299
+
300
+ ---
301
+
302
+ ## License
303
+
304
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,19 @@
1
+ network_sandbox_engine-1.0.0.dist-info/licenses/LICENSE,sha256=MFgt8rXwHCDn0xZdKsvrmoJsAmsJKBjGxTe5fnQ5-4M,1062
2
+ nse/__init__.py,sha256=H0PH7qnBwXSWXwZ2bKU9YaGQFnLK6ARw8pj74w_DLvE,137
3
+ nse/cli/__init__.py,sha256=xyEcaP29dx62zL_d7kjMfPHYou87DFwTk9z2F_TvWE0,22
4
+ nse/cli/runner.py,sha256=knMPgE_M27no3xIlz6FOdKq7vF2oFiFLPsMr753iT9w,6983
5
+ nse/core/__init__.py,sha256=Lq5e7p4Lis3O8SnVdZaPohvkjqpblAKxzJWJAtSPWew,44
6
+ nse/core/netns_controller.py,sha256=WW2WUuCup6l8gTWjDXU1i6rXi-C5vUYpo9BgOyNxDMg,18027
7
+ nse/core/pipeline.py,sha256=gQyG5F6HV43iCUdIYrAblp1KEfkQPMdjFz822DGnjbw,13603
8
+ nse/core/rule_engine.py,sha256=gcxYVFctN4vII6r3ZrHhmjDenzTJeQrEkMT-vjwwzY4,3711
9
+ nse/core/scapy_injector.py,sha256=oaLpAfw8w2Wz-wqAzVxbgM_aQWYYDrJwB9XHIqLu0AQ,7176
10
+ nse/core/sniffer.py,sha256=72aNoThXEwZViF8L1zF91GiohFaMyfLE-k13n6cA19M,1795
11
+ nse/models/__init__.py,sha256=dGqvI1YHvLMN2m3sbgNPfBfoE1b-6MBW3KsneeYVS8k,26
12
+ nse/models/base.py,sha256=0jr73qvrufmdf5l2Lx4-LGdD_7BJkOfmePxTMAdvBjg,534
13
+ nse/models/test_request.py,sha256=4QcRqucFlLHpR5CCS_5pw-UCygDGoZKDnLBhgUY7rSU,3267
14
+ nse/models/trace_event.py,sha256=vzQ4psGzVTIuaib4rQ2rqA2AWomRhczLw2VKEmkU4kw,2841
15
+ network_sandbox_engine-1.0.0.dist-info/METADATA,sha256=AX8Q8Xc56EiI0GN2G3xZ1HNFnddMbEhzOupBbIcjNUc,10951
16
+ network_sandbox_engine-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ network_sandbox_engine-1.0.0.dist-info/entry_points.txt,sha256=oLI3KtSjK-M8oiKbjBP67ixM4zgMb3dFJMc43pDNDKc,51
18
+ network_sandbox_engine-1.0.0.dist-info/top_level.txt,sha256=i3PM6tEhBAFaw7b_fn1zZy-njycoweG8M9byGRjVocw,4
19
+ network_sandbox_engine-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nse-runner = nse.cli.runner:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 onyks
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.
nse/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .core.netns_controller import NetnsController
2
+ from .core.sniffer import PCAPAsserter
3
+
4
+ __all__ = ["NetnsController", "PCAPAsserter"]
nse/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """CLI subpackage."""
nse/cli/runner.py ADDED
@@ -0,0 +1,218 @@
1
+ import sys
2
+ import argparse
3
+ import time
4
+ import asyncio
5
+
6
+ try:
7
+ import yaml
8
+ from pydantic import ValidationError
9
+ from nse.models.test_request import TestRequest, PacketSpec, TopologyType
10
+ from nse.models.trace_event import TraceEvent
11
+ from nse.core.netns_controller import NetnsController, TestRun
12
+ from nse.core.pipeline import run_test_pipeline
13
+ except ImportError:
14
+ yaml = None
15
+ ValidationError = None
16
+ TestRequest = None
17
+ PacketSpec = None
18
+ TopologyType = None
19
+ TraceEvent = None
20
+ NetnsController = None
21
+ TestRun = None
22
+ run_test_pipeline = None
23
+
24
+
25
+ def check_cli_dependencies() -> None:
26
+ """Verify that CLI extras are installed."""
27
+ if yaml is None or ValidationError is None or TestRequest is None:
28
+ print("[FATAL ERROR] Missing dependencies for CLI runner.", file=sys.stderr)
29
+ print("To use the YAML test runner, install the CLI extras:", file=sys.stderr)
30
+ print(" pip install 'network-sandbox-engine[cli]'", file=sys.stderr)
31
+ sys.exit(1)
32
+
33
+
34
+ def main() -> None:
35
+ check_cli_dependencies()
36
+ import logging
37
+
38
+ logging.basicConfig(
39
+ level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", force=True
40
+ )
41
+
42
+ parser = argparse.ArgumentParser(
43
+ prog="nse-runner",
44
+ description="Network Sandbox Engine YAML/JSON test runner",
45
+ )
46
+ parser.add_argument(
47
+ "--file",
48
+ required=True,
49
+ help="Path to the test suite YAML or JSON file",
50
+ )
51
+
52
+ args = parser.parse_args()
53
+
54
+ print(f"[NSE] Loading test suite from: {args.file}")
55
+ try:
56
+ with open(args.file, "r") as f:
57
+ if args.file.endswith(".json"):
58
+ import json
59
+
60
+ data = json.load(f)
61
+ else:
62
+ data = yaml.safe_load(f)
63
+ except Exception as e:
64
+ print(f"Error reading test suite file: {e}", file=sys.stderr)
65
+ sys.exit(1)
66
+
67
+ test_cases = data.get("tests", [])
68
+ if not test_cases:
69
+ print("No test cases found in suite.", file=sys.stderr)
70
+ sys.exit(0)
71
+
72
+ loop = asyncio.new_event_loop()
73
+ asyncio.set_event_loop(loop)
74
+
75
+ controller = NetnsController()
76
+
77
+ passed = 0
78
+ failed = 0
79
+
80
+ print(f"Found {len(test_cases)} test cases.")
81
+ print("-" * 60)
82
+
83
+ for tc_idx, tc in enumerate(test_cases):
84
+ name = tc.get("name", f"Test case {tc_idx + 1}")
85
+ print(f"Running test: {name}...")
86
+
87
+ topology_str = tc.get("topology", "simple").lower()
88
+ topology = TopologyType.GATEWAY if topology_str == "gateway" else TopologyType.SIMPLE
89
+
90
+ rules = tc.get("rules", "")
91
+
92
+ packets_data = tc.get("packets", [])
93
+ packets = []
94
+ expected_verdicts = []
95
+
96
+ for p in packets_data:
97
+ expected_verdicts.append(p.get("expected_verdict", "ACCEPT").upper())
98
+
99
+ protocol = p.get("protocol", "tcp")
100
+ src_ip = p.get("src_ip", "10.0.1.1" if topology == TopologyType.GATEWAY else "10.0.0.1")
101
+ dst_ip = p.get("dst_ip", "10.0.2.2" if topology == TopologyType.GATEWAY else "10.0.0.2")
102
+ src_port = p.get("src_port")
103
+ dst_port = p.get("dst_port")
104
+ tcp_flags = p.get("tcp_flags", [])
105
+
106
+ packets.append(
107
+ PacketSpec(
108
+ protocol=protocol,
109
+ src_ip=src_ip,
110
+ dst_ip=dst_ip,
111
+ src_port=src_port,
112
+ dst_port=dst_port,
113
+ tcp_flags=tcp_flags,
114
+ )
115
+ )
116
+
117
+ try:
118
+ request = TestRequest(rules=rules, packets=packets, topology=topology)
119
+ except ValidationError as e:
120
+ print(f" [FAIL] Test request validation failed:\n{e}")
121
+ failed += 1
122
+ continue
123
+
124
+ test_id = f"cli_{tc_idx}_{int(time.time())}"
125
+ run = TestRun(test_id=test_id, netns_name=f"nse_{test_id}", request=request)
126
+
127
+ try:
128
+ loop.run_until_complete(run_test_pipeline(controller, run))
129
+ except Exception as e:
130
+ print(f" [FAIL] Pipeline crashed with error: {e}")
131
+ failed += 1
132
+ continue
133
+
134
+ events = []
135
+ while not run.event_queue.empty():
136
+ evt = loop.run_until_complete(run.event_queue.get())
137
+ if evt is None:
138
+ continue
139
+ events.append(evt)
140
+
141
+ trace_id_to_verdicts = {}
142
+ trace_id_to_hook = {}
143
+ all_seen_trace_ids = []
144
+ errors = []
145
+
146
+ for evt in events:
147
+ if evt.type == "error":
148
+ errors.append(evt.raw_message)
149
+ elif evt.trace_id:
150
+ if evt.trace_id not in trace_id_to_verdicts:
151
+ trace_id_to_verdicts[evt.trace_id] = []
152
+ all_seen_trace_ids.append(evt.trace_id)
153
+
154
+ if evt.type == "hook" and evt.hook:
155
+ if evt.trace_id not in trace_id_to_hook:
156
+ trace_id_to_hook[evt.trace_id] = evt.hook
157
+
158
+ if evt.verdict:
159
+ trace_id_to_verdicts[evt.trace_id].append(evt.verdict.upper())
160
+
161
+ if errors:
162
+ print(f" [FAIL] Test encountered errors: {', '.join(errors)}")
163
+ failed += 1
164
+ continue
165
+
166
+ # Keep only trace IDs that enter on the expected injection hook interface
167
+ ordered_trace_ids = []
168
+ for tid in all_seen_trace_ids:
169
+ hook = trace_id_to_hook.get(tid, "")
170
+ is_valid = False
171
+ if topology == TopologyType.GATEWAY:
172
+ if hook.startswith("vrh-") or hook == "veth-nse":
173
+ is_valid = True
174
+ else:
175
+ if hook == "veth-nse":
176
+ is_valid = True
177
+
178
+ if is_valid:
179
+ ordered_trace_ids.append(tid)
180
+
181
+ actual_verdicts = []
182
+ for tid in ordered_trace_ids:
183
+ v_list = trace_id_to_verdicts[tid]
184
+ if any(v in ("DROP", "REJECT") for v in v_list):
185
+ actual_verdicts.append("DROP")
186
+ elif any(v == "ACCEPT" for v in v_list):
187
+ actual_verdicts.append("ACCEPT")
188
+ else:
189
+ actual_verdicts.append("DROP")
190
+
191
+ while len(actual_verdicts) < len(expected_verdicts):
192
+ actual_verdicts.append("DROP")
193
+
194
+ test_passed = True
195
+ for idx, (exp, act) in enumerate(zip(expected_verdicts, actual_verdicts)):
196
+ if exp != act:
197
+ print(f" [FAIL] Packet {idx + 1}: expected {exp}, got {act}")
198
+ test_passed = False
199
+ else:
200
+ print(f" [OK] Packet {idx + 1}: expected {exp}, got {act}")
201
+
202
+ if test_passed:
203
+ print(f" => SUCCESS: {name}")
204
+ passed += 1
205
+ else:
206
+ print(f" => FAILURE: {name}")
207
+ failed += 1
208
+
209
+ print("-" * 60)
210
+ print(f"Test Suite Summary: {passed} passed, {failed} failed.")
211
+ if failed > 0:
212
+ sys.exit(1)
213
+ else:
214
+ sys.exit(0)
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()
nse/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Core network sandbox engine features."""