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.
- network_sandbox_engine-1.0.0.dist-info/METADATA +304 -0
- network_sandbox_engine-1.0.0.dist-info/RECORD +19 -0
- network_sandbox_engine-1.0.0.dist-info/WHEEL +5 -0
- network_sandbox_engine-1.0.0.dist-info/entry_points.txt +2 -0
- network_sandbox_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- network_sandbox_engine-1.0.0.dist-info/top_level.txt +1 -0
- nse/__init__.py +4 -0
- nse/cli/__init__.py +1 -0
- nse/cli/runner.py +218 -0
- nse/core/__init__.py +1 -0
- nse/core/netns_controller.py +618 -0
- nse/core/pipeline.py +400 -0
- nse/core/rule_engine.py +127 -0
- nse/core/scapy_injector.py +211 -0
- nse/core/sniffer.py +55 -0
- nse/models/__init__.py +1 -0
- nse/models/base.py +22 -0
- nse/models/test_request.py +117 -0
- nse/models/trace_event.py +93 -0
|
@@ -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
|
+
[](https://pypi.org/project/network-sandbox-engine/)
|
|
44
|
+
[](https://www.python.org/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
[](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,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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nse
|
nse/__init__.py
ADDED
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."""
|