poc4 wit rust

This commit is contained in:
Mateusz Gruszczyński
2026-08-16 15:34:53 +02:00
parent e5d344622e
commit 40474cdc59
29 changed files with 2692 additions and 159 deletions
+40
View File
@@ -68,3 +68,43 @@ def test_routeros_deploy_forwards_ndr_and_persistence_controls():
"METRICS_BASIC_AUTH_PASSWORD",
):
assert f"key={key}" in script
def test_routeros_deploy_uses_hybrid_tzsp_capture_without_site_bridge_assumption():
script = (ROOT / "scripts" / "deploy-routeros.sh").read_text()
env_example = (ROOT / "deploy-routeros.env.example").read_text()
assert 'action=sniff-tzsp' in script
assert 'chain=forward' in script
assert 'comment="MikroSuricata TZSP IPv4"' in script
assert '/ip/firewall/mangle/remove [find where comment="MikroSuricata TZSP IPv4"]' in script
assert 'filter-mac-protocol=${TZSP_L2_MAC_PROTOCOL}' in script
assert 'TZSP_L2_FILTER_INTERFACE="${TZSP_L2_INTERFACE:-all}"' in script
assert 'filter-interface="${TZSP_L2_FILTER_INTERFACE}"' in script
assert 'streaming-server=${CONTAINER_IP_ONLY}:${TZSP_PORT}' in script
assert 'streaming-port=${TZSP_PORT}' in script # compatibility fallback only
assert 'VLAN_ID' not in env_example
assert 'TZSP_L2_INTERFACE=' in env_example
assert 'TZSP_L2_MAC_PROTOCOL=!ip' in env_example
assert 'bridge-trunk' not in script
def test_hybrid_capture_migration_helper_supports_dry_run_and_same_owned_rule():
script = (ROOT / "scripts" / "configure-routeros-tzsp-hybrid.sh").read_text()
assert 'DRY_RUN' in script
assert 'action=sniff-tzsp' in script
assert 'comment="MikroSuricata TZSP IPv4"' in script
assert 'filter-mac-protocol=${TZSP_L2_MAC_PROTOCOL}' in script
assert 'TZSP_L2_FILTER_INTERFACE="${TZSP_L2_INTERFACE:-all}"' in script
assert 'streaming-server=${CONTAINER_IP_ONLY}:${TZSP_PORT}' in script
def test_routeros_manual_capture_template_is_hybrid_and_not_vlan_specific():
hybrid = ROOT / "routeros" / "02-tzsp-hybrid.rsc"
assert hybrid.exists()
assert not (ROOT / "routeros" / "02-sniffer-vlan100.rsc").exists()
text = hybrid.read_text()
assert 'action=sniff-tzsp' in text
assert 'filter-mac-protocol=$l2MacProtocol' in text
assert ':local l2Interface "all"' in text
assert 'filter-vlan=100' not in text
+44
View File
@@ -27,6 +27,50 @@ class _Status:
class PrometheusMetricsTests(unittest.TestCase):
def test_render_exports_rust_receiver_quality_metrics(self):
stats = RuntimeStats()
metrics = PrometheusMetrics(
stats,
version="0.11.0",
mode="full",
started_at=datetime(2026, 8, 16, 6, 0, tzinfo=timezone.utc),
state_provider=lambda: {"components": {}, "features": {}},
flow_tracker=_Status(
engine="rust",
rcvbuf_bytes=67_108_864,
batch_size=256,
datagram_bytes=12288,
queue_capacity_batches=24,
queue_capacity_bytes=67_108_864,
queue_depth_batches=2,
queue_high_water_batches=9,
capture_efficiency_pct=99.9,
rx_thread_alive=True,
worker_thread_alive=True,
telemetry_age_ms=125,
process_alive=True,
ready=True,
kernel_udp_drops=7,
queue_dropped_datagrams=11,
truncated_datagrams=0,
telemetry_errors=0,
traffic_counters={
"bytes_total": 125_000_000,
"packets_total": 80_000,
},
),
)
rendered = metrics.render()
self.assertIn('mikrosuricata_tzsp_receiver_info{engine="rust"} 1', rendered)
self.assertIn("mikrosuricata_tzsp_receiver_rcvbuf_bytes 67108864", rendered)
self.assertIn("mikrosuricata_tzsp_receiver_batch_size 256", rendered)
self.assertIn("mikrosuricata_tzsp_receiver_queue_depth_batches 2", rendered)
self.assertIn("mikrosuricata_tzsp_receiver_capture_efficiency_pct 99.9", rendered)
self.assertIn("mikrosuricata_tzsp_receiver_kernel_udp_drops_total 7", rendered)
self.assertIn("mikrosuricata_tzsp_receiver_queue_dropped_datagrams_total 11", rendered)
self.assertIn('mikrosuricata_traffic_bytes_total{direction="total"} 125000000', rendered)
self.assertNotIn("mikrosuricata_flow_tracker_", rendered)
def test_render_exports_raw_runtime_suricata_and_in_memory_component_state(self):
stats = RuntimeStats()
stats.inc("tzsp_datagrams", 5)
+36
View File
@@ -0,0 +1,36 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_production_main_uses_rust_receiver_not_python_packet_receiver():
main = (ROOT / "app" / "main.py").read_text()
assert "from .tzsp_rust import RustTZSPReceiver" in main
assert "from .tzsp import TZSPReceiver" not in main
assert "TapDevice(" not in main
assert "flow_tracker.observe(" not in main
def test_docker_builds_and_tests_rust_receiver_before_runtime_copy():
dockerfile = (ROOT / "Dockerfile").read_text()
assert "AS rust-builder" in dockerfile
assert "cargo test --release" in dockerfile
assert "cargo build --release" in dockerfile
assert "COPY --from=rust-builder" in dockerfile
assert "/usr/local/bin/mikrosuricata-tzsp" in dockerfile
def test_rust_receiver_is_dependency_free_and_uses_batched_linux_receive():
cargo = (ROOT / "rust" / "tzsp-receiver" / "Cargo.toml").read_text()
source = (ROOT / "rust" / "tzsp-receiver" / "src" / "main.rs").read_text()
assert "[dependencies]" not in cargo
assert "recvmmsg(" in source
assert "MSG_WAITFORONE" in source
assert "sync_channel::<BatchBuffer>" in source
assert 'name("tzsp-udp-rx"' in source
assert 'name("tzsp-tap-worker"' in source
assert "queue_dropped_datagrams" in source
assert "SO_RCVBUFFORCE" in source
assert "TUNSETIFF" in source
assert "tap.write(frame)" in source
assert "write_all(frame)" not in source
+112
View File
@@ -0,0 +1,112 @@
from __future__ import annotations
import threading
import time
import unittest
from app.state import RuntimeStats
from app.tzsp_rust import RustTZSPReceiver
class RustTZSPReceiverTelemetryTests(unittest.TestCase):
def _receiver(self, sink=None):
return RustTZSPReceiver(
binary="/does/not/start-in-unit-tests",
telemetry_socket="/tmp/unused-tzsp-test.sock",
stats=RuntimeStats(),
stop_event=threading.Event(),
throughput_sink=sink,
)
def test_telemetry_sample_drives_live_rate_without_packet_bytes_in_python(self):
written = []
receiver = self._receiver(written.append)
now_ms = int(time.time() * 1000)
receiver._ingest(
{
"type": "tzsp_sample",
"engine": "rust",
"ready": True,
"ts_ms": now_ms,
"interval_ms": 1000,
"last_packet_ms": now_ms,
"bytes_total": 125_000_000,
"bytes_in": 100_000_000,
"bytes_out": 25_000_000,
"packets_total": 80_000,
"traffic_counters": {"bytes_total": 125_000_000, "packets_total": 80_000},
"kernel_udp_drops": 3,
"kernel_udp_drops_interval": 0,
"queue_dropped_datagrams": 0,
"queue_drops_interval": 0,
"truncated_datagrams": 0,
"truncated_interval": 0,
"queue_depth_batches": 1,
"queue_capacity_batches": 24,
"queue_capacity_bytes": 64 * 1024 * 1024,
"capture_efficiency_pct": 100.0,
"rx_datagrams_interval": 80_000,
"rx_bytes_interval": 126_000_000,
"rx_thread_alive": True,
"worker_thread_alive": True,
"rcvbuf_bytes": 425_984,
"batch_size": 256,
"datagram_bytes": 12_288,
}
)
current = receiver.current_throughput(3600)
self.assertEqual(current["current_bps"], 1_000_000_000)
self.assertEqual(current["current_in_bps"], 800_000_000)
self.assertEqual(current["current_out_bps"], 200_000_000)
self.assertEqual(current["current_pps"], 80_000)
self.assertEqual(current["kernel_udp_drops"], 3)
self.assertEqual(current["current_ingress_bps"], 1_008_000_000)
self.assertEqual(current["capture_efficiency_pct"], 100.0)
self.assertEqual(current["queue_fill_pct"], 4.2)
self.assertEqual(current["loss_pps"], 0)
self.assertTrue(current["rx_thread_alive"])
self.assertTrue(current["worker_thread_alive"])
self.assertEqual(current["receiver_engine"], "rust")
self.assertEqual(len(written), 1)
self.assertEqual(written[0]["bytes_total"], 125_000_000)
def test_ingress_and_inspection_are_reported_separately_when_pipeline_is_behind(self):
receiver = self._receiver()
now_ms = int(time.time() * 1000)
receiver._last = {
"ts_ms": now_ms,
"interval_ms": 1000,
"bytes_total": 31_250_000,
"packets_total": 24_000,
"rx_bytes_interval": 125_000_000,
"rx_datagrams_interval": 80_000,
"kernel_udp_drops_interval": 10,
"queue_drops_interval": 90,
"truncated_interval": 0,
"queue_depth_batches": 20,
"queue_capacity_batches": 22,
"capture_efficiency_pct": 99.875,
}
current = receiver.current_throughput(900)
self.assertEqual(current["current_bps"], 250_000_000)
self.assertEqual(current["current_ingress_bps"], 1_000_000_000)
self.assertEqual(current["inspection_ratio_pct"], 25.0)
self.assertEqual(current["queue_fill_pct"], 90.9)
self.assertEqual(current["loss_pps"], 100.0)
def test_stale_sample_reports_zero_current_rate(self):
receiver = self._receiver()
receiver._last = {
"ts_ms": int(time.time() * 1000) - 10_000,
"interval_ms": 1000,
"bytes_total": 125_000_000,
"packets_total": 80_000,
}
current = receiver.current_throughput(900)
self.assertFalse(current["current_sample_fresh"])
self.assertEqual(current["current_bps"], 0)
self.assertEqual(current["current_pps"], 0)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -65,7 +65,7 @@ class WebUITests(unittest.TestCase):
self.assertIn(f'data-subtab-panel="intelligence:{tab}"', DASHBOARD)
self.assertIn("Asset intelligence", DASHBOARD)
self.assertIn("Threat intelligence repository", DASHBOARD)
self.assertIn("Forensic PCAP ring", DASHBOARD)
self.assertIn("Forensic PCAP", DASHBOARD)
def test_system_has_dedicated_redis_status_panel(self):
self.assertIn('<h2>Redis</h2>', DASHBOARD)