poc4 wit rust
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user